mirror of
https://github.com/mempool/mempool.git
synced 2025-03-17 13:21:55 +01:00
Merge branch 'master' into nymkappa/content-size-limit
This commit is contained in:
commit
3d309f50fa
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 }}"
|
28
.github/workflows/on-tag.yml
vendored
28
.github/workflows/on-tag.yml
vendored
@ -2,7 +2,7 @@ name: Docker build on tag
|
||||
env:
|
||||
DOCKER_CLI_EXPERIMENTAL: enabled
|
||||
TAG_FMT: "^refs/tags/(((.?[0-9]+){3,4}))$"
|
||||
DOCKER_BUILDKIT: 0
|
||||
DOCKER_BUILDKIT: 1 # Enable BuildKit for better performance
|
||||
COMPOSE_DOCKER_CLI_BUILD: 0
|
||||
|
||||
on:
|
||||
@ -25,13 +25,12 @@ jobs:
|
||||
timeout-minutes: 120
|
||||
name: Build and push to DockerHub
|
||||
steps:
|
||||
# Workaround based on JonasAlfredsson/docker-on-tmpfs@v1.0.1
|
||||
- name: Replace the current swap file
|
||||
shell: bash
|
||||
run: |
|
||||
sudo swapoff /mnt/swapfile
|
||||
sudo rm -v /mnt/swapfile
|
||||
sudo fallocate -l 13G /mnt/swapfile
|
||||
sudo swapoff /mnt/swapfile || true
|
||||
sudo rm -f /mnt/swapfile
|
||||
sudo fallocate -l 16G /mnt/swapfile
|
||||
sudo chmod 600 /mnt/swapfile
|
||||
sudo mkswap /mnt/swapfile
|
||||
sudo swapon /mnt/swapfile
|
||||
@ -50,7 +49,7 @@ jobs:
|
||||
echo "Directory '/var/lib/docker' not found"
|
||||
exit 1
|
||||
fi
|
||||
sudo mount -t tmpfs -o size=10G tmpfs /var/lib/docker
|
||||
sudo mount -t tmpfs -o size=12G tmpfs /var/lib/docker
|
||||
sudo systemctl restart docker
|
||||
sudo df -h | grep docker
|
||||
|
||||
@ -75,10 +74,16 @@ jobs:
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
id: qemu
|
||||
|
||||
- name: Setup Docker buildx action
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
driver-opts: |
|
||||
network=host
|
||||
id: buildx
|
||||
|
||||
- name: Available platforms
|
||||
@ -89,19 +94,20 @@ jobs:
|
||||
id: cache
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
key: ${{ runner.os }}-buildx-${{ matrix.service }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
${{ runner.os }}-buildx-${{ matrix.service }}-
|
||||
|
||||
- name: Run Docker buildx for ${{ matrix.service }} against tag
|
||||
run: |
|
||||
docker buildx build \
|
||||
--cache-from "type=local,src=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache" \
|
||||
--cache-to "type=local,dest=/tmp/.buildx-cache,mode=max" \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:$TAG \
|
||||
--tag ${{ secrets.DOCKER_HUB_USER }}/${{ matrix.service }}:latest \
|
||||
--build-context rustgbt=./rust \
|
||||
--build-context backend=./backend \
|
||||
--output "type=registry" ./${{ matrix.service }}/ \
|
||||
--build-arg commitHash=$SHORT_SHA
|
||||
--output "type=registry,push=true" \
|
||||
--build-arg commitHash=$SHORT_SHA \
|
||||
./${{ matrix.service }}/
|
28
backend/package-lock.json
generated
28
backend/package-lock.json
generated
@ -12,12 +12,12 @@
|
||||
"dependencies": {
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "1.7.2",
|
||||
"axios": "1.8.1",
|
||||
"bitcoinjs-lib": "~6.1.3",
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.21.1",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.12.0",
|
||||
"mysql2": "~3.13.0",
|
||||
"redis": "^4.7.0",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
@ -2275,9 +2275,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
||||
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
|
||||
"integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
@ -6173,9 +6173,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
|
||||
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.13.0.tgz",
|
||||
"integrity": "sha512-M6DIQjTqKeqXH5HLbLMxwcK5XfXHw30u5ap6EZmu7QVmcF/gnh2wS/EOiQ4MTbXz/vQeoXrmycPlVRM00WSslg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"aws-ssl-profiles": "^1.1.1",
|
||||
@ -9459,9 +9459,9 @@
|
||||
"integrity": "sha512-+H+kuK34PfMaI9PNU/NSjBKL5hh/KDM9J72kwYeYEm0A8B1AC4fuCy3qsjnA7lxklgyXsB68yn8Z2xoZEjgwCQ=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
|
||||
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
|
||||
"integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
@ -12337,9 +12337,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"mysql2": {
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz",
|
||||
"integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==",
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.13.0.tgz",
|
||||
"integrity": "sha512-M6DIQjTqKeqXH5HLbLMxwcK5XfXHw30u5ap6EZmu7QVmcF/gnh2wS/EOiQ4MTbXz/vQeoXrmycPlVRM00WSslg==",
|
||||
"requires": {
|
||||
"aws-ssl-profiles": "^1.1.1",
|
||||
"denque": "^2.1.0",
|
||||
|
@ -41,12 +41,12 @@
|
||||
"dependencies": {
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "1.7.2",
|
||||
"axios": "1.8.1",
|
||||
"bitcoinjs-lib": "~6.1.3",
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.21.1",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.12.0",
|
||||
"mysql2": "~3.13.0",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"redis": "^4.7.0",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
|
@ -55,6 +55,8 @@ 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))
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'prevouts', this.$getPrevouts)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'cpfp', this.getCpfpLocalTxs)
|
||||
// Temporarily add txs/package endpoint for all backends until esplora supports it
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
|
||||
// Internal routes
|
||||
@ -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();
|
||||
|
@ -167,8 +167,10 @@ export function calculateGoodBlockCpfp(height: number, transactions: MempoolTran
|
||||
/**
|
||||
* Takes a mempool transaction and a copy of the current mempool, and calculates the CPFP data for
|
||||
* that transaction (and all others in the same cluster)
|
||||
* If the passed transaction is not guaranteed to be in the mempool, set localTx to true: this will
|
||||
* prevent updating the CPFP data of other transactions in the cluster
|
||||
*/
|
||||
export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }): CpfpInfo {
|
||||
export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool: { [txid: string]: MempoolTransactionExtended }, localTx: boolean = false): CpfpInfo {
|
||||
if (tx.cpfpUpdated && Date.now() < (tx.cpfpUpdated + CPFP_UPDATE_INTERVAL)) {
|
||||
tx.cpfpDirty = false;
|
||||
return {
|
||||
@ -198,17 +200,26 @@ export function calculateMempoolTxCpfp(tx: MempoolTransactionExtended, mempool:
|
||||
totalFee += tx.fees.base;
|
||||
}
|
||||
const effectiveFeePerVsize = totalFee / totalVsize;
|
||||
for (const tx of cluster.values()) {
|
||||
mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
|
||||
mempool[tx.txid].ancestors = Array.from(tx.ancestors.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
|
||||
mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
|
||||
mempool[tx.txid].bestDescendant = null;
|
||||
mempool[tx.txid].cpfpChecked = true;
|
||||
mempool[tx.txid].cpfpDirty = true;
|
||||
mempool[tx.txid].cpfpUpdated = Date.now();
|
||||
}
|
||||
|
||||
tx = mempool[tx.txid];
|
||||
if (localTx) {
|
||||
tx.effectiveFeePerVsize = effectiveFeePerVsize;
|
||||
tx.ancestors = Array.from(cluster.get(tx.txid)?.ancestors.values() || []).map(ancestor => ({ txid: ancestor.txid, weight: ancestor.weight, fee: ancestor.fees.base }));
|
||||
tx.descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !cluster.get(tx.txid)?.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
|
||||
tx.bestDescendant = null;
|
||||
} else {
|
||||
for (const tx of cluster.values()) {
|
||||
mempool[tx.txid].effectiveFeePerVsize = effectiveFeePerVsize;
|
||||
mempool[tx.txid].ancestors = Array.from(tx.ancestors.values()).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
|
||||
mempool[tx.txid].descendants = Array.from(cluster.values()).filter(entry => entry.txid !== tx.txid && !tx.ancestors.has(entry.txid)).map(tx => ({ txid: tx.txid, weight: tx.weight, fee: tx.fees.base }));
|
||||
mempool[tx.txid].bestDescendant = null;
|
||||
mempool[tx.txid].cpfpChecked = true;
|
||||
mempool[tx.txid].cpfpDirty = true;
|
||||
mempool[tx.txid].cpfpUpdated = Date.now();
|
||||
}
|
||||
|
||||
tx = mempool[tx.txid];
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
ancestors: tx.ancestors || [],
|
||||
|
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 95;
|
||||
private static currentVersion = 96;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@ -1130,6 +1130,11 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE blocks ADD INDEX `definition_hash` (`definition_hash`)');
|
||||
await this.updateToSchemaVersion(95);
|
||||
}
|
||||
|
||||
if (databaseSchemaVersion < 96) {
|
||||
await this.$executeQuery(`ALTER TABLE blocks_audits MODIFY time timestamp NOT NULL DEFAULT 0`);
|
||||
await this.updateToSchemaVersion(96);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -420,6 +420,29 @@ class TransactionUtils {
|
||||
|
||||
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();
|
||||
|
@ -1,20 +1,20 @@
|
||||
FROM node:20.15.0-buster-slim AS builder
|
||||
FROM rust:1.84-bookworm AS builder
|
||||
|
||||
ARG commitHash
|
||||
ENV MEMPOOL_COMMIT_HASH=${commitHash}
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
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 build-essential python3 pkg-config && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN apt-get update
|
||||
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"
|
||||
ENV PATH="/usr/local/cargo/bin:$PATH"
|
||||
|
||||
COPY --from=backend . .
|
||||
COPY --from=rustgbt . ../rust/
|
||||
@ -24,7 +24,14 @@ RUN npm install --omit=dev --omit=optional
|
||||
WORKDIR /build
|
||||
RUN npm run package
|
||||
|
||||
FROM node:20.15.0-buster-slim
|
||||
FROM rust:1.84-bookworm AS runtime
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y curl ca-certificates && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
|
||||
apt-get install -y nodejs && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM node:20.15.0-buster-slim AS builder
|
||||
FROM node:22-bookworm-slim AS builder
|
||||
|
||||
ARG commitHash
|
||||
ENV DOCKER_COMMIT_HASH=${commitHash}
|
||||
|
@ -3,10 +3,10 @@ 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';
|
||||
const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.va1.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");
|
||||
entry.target = entry.target.replace("liquid.network", "liquid-staging.va1.mempool.space");
|
||||
});
|
||||
|
||||
module.exports = PROXY_CONFIG;
|
||||
|
@ -12,6 +12,7 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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">
|
||||
|
@ -36,6 +36,8 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
sent = 0;
|
||||
totalUnspent = 0;
|
||||
|
||||
ogSession: number;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
@ -58,7 +60,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
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.isLoadingAddress = true;
|
||||
this.loadedConfirmedTxCount = 0;
|
||||
@ -79,7 +81,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
this.isLoadingAddress = false;
|
||||
this.error = 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);
|
||||
})
|
||||
);
|
||||
@ -97,7 +99,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
this.address = address;
|
||||
this.updateChainStats();
|
||||
this.isLoadingAddress = false;
|
||||
this.openGraphService.waitOver('address-data-' + this.rawAddress);
|
||||
this.openGraphService.waitOver({ event: 'address-data-' + this.rawAddress, sessionId: this.ogSession });
|
||||
})
|
||||
)
|
||||
.subscribe(() => {},
|
||||
@ -105,7 +107,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
console.log(error);
|
||||
this.error = error;
|
||||
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>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet' && block?.extras?.pool">
|
||||
<td i18n="block.miner">Miner</td>
|
||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||
<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;
|
||||
networkChangedSubscription: Subscription;
|
||||
|
||||
ogSession: number;
|
||||
|
||||
@ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
|
||||
|
||||
constructor(
|
||||
@ -53,8 +55,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
const block$ = this.route.paramMap.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.rawId = params.get('id') || '';
|
||||
this.openGraphService.waitFor('block-viz-' + this.rawId);
|
||||
this.openGraphService.waitFor('block-data-' + this.rawId);
|
||||
this.ogSession = this.openGraphService.waitFor('block-viz-' + this.rawId);
|
||||
this.ogSession = this.openGraphService.waitFor('block-data-' + this.rawId);
|
||||
|
||||
const blockHash: string = params.get('id') || '';
|
||||
this.block = undefined;
|
||||
@ -86,8 +88,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
this.seoService.logSoft404();
|
||||
this.openGraphService.fail('block-data-' + this.rawId);
|
||||
this.openGraphService.fail('block-viz-' + this.rawId);
|
||||
this.openGraphService.fail({ event: 'block-data-' + this.rawId, sessionId: this.ogSession });
|
||||
this.openGraphService.fail({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession });
|
||||
return of(null);
|
||||
}),
|
||||
);
|
||||
@ -114,7 +116,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
this.isLoadingOverview = true;
|
||||
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 }),
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
@ -129,7 +131,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
this.overviewError = err;
|
||||
this.openGraphService.fail('block-viz-' + this.rawId);
|
||||
this.openGraphService.fail({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession });
|
||||
return of([]);
|
||||
}),
|
||||
switchMap((transactions) => {
|
||||
@ -138,7 +140,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
),
|
||||
this.stateService.env.ACCELERATOR === true && block.height > 819500
|
||||
? this.servicesApiService.getAllAccelerationHistory$({ blockHeight: block.height })
|
||||
.pipe(catchError(() => {
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
return of([]);
|
||||
}))
|
||||
: of([])
|
||||
@ -169,8 +172,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
this.error = error;
|
||||
this.isLoadingOverview = false;
|
||||
this.seoService.logSoft404();
|
||||
this.openGraphService.fail('block-viz-' + this.rawId);
|
||||
this.openGraphService.fail('block-data-' + this.rawId);
|
||||
this.openGraphService.fail({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession });
|
||||
this.openGraphService.fail({ event: 'block-data-' + this.rawId, sessionId: this.ogSession });
|
||||
if (this.blockGraph) {
|
||||
this.blockGraph.destroy();
|
||||
}
|
||||
@ -196,6 +199,6 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onGraphReady(): void {
|
||||
this.openGraphService.waitOver('block-viz-' + this.rawId);
|
||||
this.openGraphService.waitOver({ event: 'block-viz-' + this.rawId, sessionId: this.ogSession });
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,8 @@ export class PoolPreviewComponent implements OnInit {
|
||||
|
||||
slug: string = undefined;
|
||||
|
||||
ogSession: number;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private apiService: ApiService,
|
||||
@ -47,22 +49,22 @@ export class PoolPreviewComponent implements OnInit {
|
||||
this.isLoading = true;
|
||||
this.imageLoaded = false;
|
||||
this.slug = slug;
|
||||
this.openGraphService.waitFor('pool-hash-' + this.slug);
|
||||
this.openGraphService.waitFor('pool-stats-' + this.slug);
|
||||
this.openGraphService.waitFor('pool-chart-' + this.slug);
|
||||
this.openGraphService.waitFor('pool-img-' + this.slug);
|
||||
this.ogSession = this.openGraphService.waitFor('pool-hash-' + this.slug);
|
||||
this.ogSession = this.openGraphService.waitFor('pool-stats-' + this.slug);
|
||||
this.ogSession = this.openGraphService.waitFor('pool-chart-' + this.slug);
|
||||
this.ogSession = this.openGraphService.waitFor('pool-img-' + this.slug);
|
||||
return this.apiService.getPoolHashrate$(this.slug)
|
||||
.pipe(
|
||||
switchMap((data) => {
|
||||
this.isLoading = false;
|
||||
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];
|
||||
}),
|
||||
catchError(() => {
|
||||
this.isLoading = false;
|
||||
this.seoService.logSoft404();
|
||||
this.openGraphService.fail('pool-hash-' + this.slug);
|
||||
this.openGraphService.fail({ event: 'pool-hash-' + this.slug, sessionId: this.ogSession });
|
||||
return of([slug]);
|
||||
})
|
||||
);
|
||||
@ -72,7 +74,7 @@ export class PoolPreviewComponent implements OnInit {
|
||||
catchError(() => {
|
||||
this.isLoading = false;
|
||||
this.seoService.logSoft404();
|
||||
this.openGraphService.fail('pool-stats-' + this.slug);
|
||||
this.openGraphService.fail({ event: 'pool-stats-' + this.slug, sessionId: this.ogSession });
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
@ -90,11 +92,11 @@ export class PoolPreviewComponent implements OnInit {
|
||||
}
|
||||
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';
|
||||
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;
|
||||
return Object.assign({
|
||||
@ -103,7 +105,7 @@ export class PoolPreviewComponent implements OnInit {
|
||||
}),
|
||||
catchError(() => {
|
||||
this.isLoading = false;
|
||||
this.openGraphService.fail('pool-stats-' + this.slug);
|
||||
this.openGraphService.fail({ event: 'pool-stats-' + this.slug, sessionId: this.ogSession });
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
@ -170,16 +172,16 @@ export class PoolPreviewComponent implements OnInit {
|
||||
}
|
||||
|
||||
onChartReady(): void {
|
||||
this.openGraphService.waitOver('pool-chart-' + this.slug);
|
||||
this.openGraphService.waitOver({ event: 'pool-chart-' + this.slug, sessionId: this.ogSession });
|
||||
}
|
||||
|
||||
onImageLoad(): void {
|
||||
this.imageLoaded = true;
|
||||
this.openGraphService.waitOver('pool-img-' + this.slug);
|
||||
this.openGraphService.waitOver({ event: 'pool-img-' + this.slug, sessionId: this.ogSession });
|
||||
}
|
||||
|
||||
onImageFail(): void {
|
||||
this.imageLoaded = false;
|
||||
this.openGraphService.waitOver('pool-img-' + this.slug);
|
||||
this.openGraphService.waitOver({ event: 'pool-img-' + this.slug, sessionId: this.ogSession });
|
||||
}
|
||||
}
|
||||
|
@ -194,14 +194,16 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
|
||||
applyScrollLeft(): void {
|
||||
if (this.blockchainContainer?.nativeElement?.scrollWidth) {
|
||||
let lastScrollLeft = null;
|
||||
while (this.scrollLeft < 0 && this.shiftPagesForward() && lastScrollLeft !== this.scrollLeft) {
|
||||
lastScrollLeft = this.scrollLeft;
|
||||
this.scrollLeft += this.pageWidth;
|
||||
}
|
||||
lastScrollLeft = null;
|
||||
while (this.scrollLeft > this.blockchainContainer.nativeElement.scrollWidth && this.shiftPagesBack() && lastScrollLeft !== this.scrollLeft) {
|
||||
lastScrollLeft = this.scrollLeft;
|
||||
this.scrollLeft -= this.pageWidth;
|
||||
if (!this.timeLtr) {
|
||||
while (this.scrollLeft < 0 && this.shiftPagesForward() && lastScrollLeft !== this.scrollLeft) {
|
||||
lastScrollLeft = this.scrollLeft;
|
||||
this.scrollLeft += this.pageWidth;
|
||||
}
|
||||
lastScrollLeft = null;
|
||||
while (this.scrollLeft > this.blockchainContainer.nativeElement.scrollWidth && this.shiftPagesBack() && lastScrollLeft !== this.scrollLeft) {
|
||||
lastScrollLeft = this.scrollLeft;
|
||||
this.scrollLeft -= this.pageWidth;
|
||||
}
|
||||
}
|
||||
this.blockchainContainer.nativeElement.scrollLeft = this.scrollLeft;
|
||||
}
|
||||
|
@ -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>
|
||||
@if (!isLoadingTx) {
|
||||
@if (!replaced && !isCached) {
|
||||
@if (!replaced && !isCached && !unbroadcasted) {
|
||||
<tr>
|
||||
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
|
||||
<td>
|
||||
@ -184,7 +184,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
} @else {
|
||||
} @else if (!unbroadcasted){
|
||||
<ng-container *ngTemplateOutlet="skeletonDetailsRow"></ng-container>
|
||||
}
|
||||
</ng-template>
|
||||
@ -213,11 +213,11 @@
|
||||
@if (!isLoadingTx) {
|
||||
<tr>
|
||||
<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) {
|
||||
<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>
|
||||
</tr>
|
||||
} @else {
|
||||
|
@ -38,6 +38,7 @@ export class TransactionDetailsComponent implements OnInit {
|
||||
@Input() replaced: boolean;
|
||||
@Input() isCached: boolean;
|
||||
@Input() ETA$: Observable<ETA>;
|
||||
@Input() unbroadcasted: boolean;
|
||||
|
||||
@Output() accelerateClicked = new EventEmitter<boolean>();
|
||||
@Output() toggleCpfp$ = new EventEmitter<void>();
|
||||
|
@ -43,6 +43,8 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
opReturns: Vout[];
|
||||
extraData: 'none' | 'coinbase' | 'opreturn';
|
||||
|
||||
ogSession: number;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
@ -75,7 +77,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
.subscribe((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
|
||||
@ -83,8 +85,8 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
switchMap((params: ParamMap) => {
|
||||
const urlMatch = (params.get('id') || '').split(':');
|
||||
this.txId = urlMatch[0];
|
||||
this.openGraphService.waitFor('tx-data-' + this.txId);
|
||||
this.openGraphService.waitFor('tx-time-' + this.txId);
|
||||
this.ogSession = this.openGraphService.waitFor('tx-data-' + this.txId);
|
||||
this.ogSession = this.openGraphService.waitFor('tx-time-' + this.txId);
|
||||
this.seoService.setTitle(
|
||||
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
|
||||
);
|
||||
@ -138,7 +140,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
.subscribe((tx: Transaction) => {
|
||||
if (!tx) {
|
||||
this.seoService.logSoft404();
|
||||
this.openGraphService.fail('tx-data-' + this.txId);
|
||||
this.openGraphService.fail({ event: 'tx-data-' + this.txId, sessionId: this.ogSession });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -155,10 +157,10 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (tx.status.confirmed) {
|
||||
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) {
|
||||
this.transactionTime = tx.firstSeen;
|
||||
this.openGraphService.waitOver('tx-time-' + this.txId);
|
||||
this.openGraphService.waitOver({ event: 'tx-time-' + this.txId, sessionId: this.ogSession });
|
||||
} else {
|
||||
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) => {
|
||||
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.isLoadingTx = false;
|
||||
}
|
||||
@ -205,7 +207,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
.subscribe((transactionTimes) => {
|
||||
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,212 @@
|
||||
<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 *ngIf="successBroadcast" type="button" class="btn btn-sm btn-success no-cursor" i18n="transaction.broadcasted|Broadcasted">Broadcasted</button>
|
||||
<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 *ngIf="!successBroadcast" class="alert alert-mempool" style="align-items: center;">
|
||||
<span>
|
||||
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
|
||||
<ng-container 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>
|
||||
</span>
|
||||
<button [disabled]="isLoadingBroadcast" type="button" class="btn btn-sm btn-primary" i18n="transaction.broadcast|Broadcast" (click)="postTx()">Broadcast</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,194 @@
|
||||
.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;
|
||||
}
|
@ -0,0 +1,313 @@
|
||||
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 { firstValueFrom, Subscription } 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;
|
||||
|
||||
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, 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,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async postTx(): Promise<string> {
|
||||
this.isLoadingBroadcast = true;
|
||||
this.errorBroadcast = null;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.apiService.postTransaction$(this.rawHexTransaction)
|
||||
.subscribe((result) => {
|
||||
this.isLoadingBroadcast = false;
|
||||
this.successBroadcast = true;
|
||||
this.transaction.txid = result;
|
||||
resolve(result);
|
||||
},
|
||||
(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;
|
||||
reject(this.error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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({});
|
||||
}
|
||||
|
||||
}
|
@ -67,64 +67,7 @@
|
||||
<ng-template [ngIf]="!isLoadingTx && !error">
|
||||
|
||||
<!-- CPFP Details -->
|
||||
<ng-template [ngIf]="showCpfpDetails">
|
||||
<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>
|
||||
<app-cpfp-info *ngIf="showCpfpDetails" [cpfpInfo]="cpfpInfo" [tx]="tx"></app-cpfp-info>
|
||||
|
||||
<!-- Accelerator -->
|
||||
<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 {
|
||||
.alert-link {
|
||||
display: block;
|
||||
|
@ -1049,10 +1049,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.stateService.markBlock$.next({});
|
||||
}
|
||||
|
||||
roundToOneDecimal(cpfpTx: any): number {
|
||||
return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1);
|
||||
}
|
||||
|
||||
setupGraph() {
|
||||
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);
|
||||
|
@ -9,6 +9,8 @@ import { TransactionExtrasModule } from '@components/transaction/transaction-ext
|
||||
import { GraphsModule } from '@app/graphs/graphs.module';
|
||||
import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.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 = [
|
||||
{
|
||||
@ -16,6 +18,10 @@ const routes: Routes = [
|
||||
redirectTo: '/',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'preview',
|
||||
component: TransactionRawComponent,
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
component: TransactionComponent,
|
||||
@ -49,12 +55,15 @@ export class TransactionRoutingModule { }
|
||||
TransactionDetailsComponent,
|
||||
AccelerateCheckout,
|
||||
AccelerateFeeGraphComponent,
|
||||
TransactionRawComponent,
|
||||
CpfpInfoComponent,
|
||||
],
|
||||
exports: [
|
||||
TransactionComponent,
|
||||
TransactionDetailsComponent,
|
||||
AccelerateCheckout,
|
||||
AccelerateFeeGraphComponent,
|
||||
CpfpInfoComponent,
|
||||
]
|
||||
})
|
||||
export class TransactionModule { }
|
||||
|
@ -37,6 +37,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
@Input() addresses: string[] = [];
|
||||
@Input() rowLimit = 12;
|
||||
@Input() blockTime: number = 0; // Used for price calculation if all the transactions are in the same block
|
||||
@Input() txPreview = false;
|
||||
|
||||
@Output() loadMore = new EventEmitter();
|
||||
|
||||
@ -81,7 +82,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
this.refreshOutspends$
|
||||
.pipe(
|
||||
switchMap((txIds) => {
|
||||
if (!this.cached) {
|
||||
if (!this.cached && !this.txPreview) {
|
||||
// break list into batches of 50 (maximum supported by esplora)
|
||||
const batches = [];
|
||||
for (let i = 0; i < txIds.length; i += 50) {
|
||||
@ -119,7 +120,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
),
|
||||
this.refreshChannels$
|
||||
.pipe(
|
||||
filter(() => this.stateService.networkSupportsLightning()),
|
||||
filter(() => this.stateService.networkSupportsLightning() && !this.txPreview),
|
||||
switchMap((txIds) => this.apiService.getChannelByTxIds$(txIds)),
|
||||
catchError((error) => {
|
||||
// handle 404
|
||||
@ -187,7 +188,10 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
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;
|
||||
this.transactions.forEach((tx) => {
|
||||
@ -351,7 +355,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
loadMoreInputs(tx: Transaction): void {
|
||||
if (!tx['@vinLoaded']) {
|
||||
if (!tx['@vinLoaded'] && !this.txPreview) {
|
||||
this.electrsApiService.getTransaction$(tx.txid)
|
||||
.subscribe((newTx) => {
|
||||
tx['@vinLoaded'] = true;
|
||||
|
@ -34,29 +34,39 @@ export class TwitterWidgetComponent implements OnChanges {
|
||||
}
|
||||
|
||||
setIframeSrc(): void {
|
||||
if (this.handle) {
|
||||
this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.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'
|
||||
));
|
||||
if (!this.handle) {
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
|
@ -125,6 +125,8 @@ export class WalletPreviewComponent implements OnInit, OnDestroy {
|
||||
sent = 0;
|
||||
chainBalance = 0;
|
||||
|
||||
ogSession: number;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private stateService: StateService,
|
||||
@ -141,9 +143,9 @@ export class WalletPreviewComponent implements OnInit, OnDestroy {
|
||||
map((params: ParamMap) => params.get('wallet') as string),
|
||||
tap((walletName: string) => {
|
||||
this.walletName = walletName;
|
||||
this.openGraphService.waitFor('wallet-addresses-' + this.walletName);
|
||||
this.openGraphService.waitFor('wallet-data-' + this.walletName);
|
||||
this.openGraphService.waitFor('wallet-txs-' + this.walletName);
|
||||
this.ogSession = this.openGraphService.waitFor('wallet-addresses-' + this.walletName);
|
||||
this.ogSession = this.openGraphService.waitFor('wallet-data-' + this.walletName);
|
||||
this.ogSession = this.openGraphService.waitFor('wallet-txs-' + this.walletName);
|
||||
this.seoService.setTitle($localize`:@@wallet.component.browser-title:Wallet: ${walletName}:INTERPOLATION:`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.wallet:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} wallet ${walletName}:INTERPOLATION:.`);
|
||||
}),
|
||||
@ -152,9 +154,9 @@ export class WalletPreviewComponent implements OnInit, OnDestroy {
|
||||
this.error = err;
|
||||
this.seoService.logSoft404();
|
||||
console.log(err);
|
||||
this.openGraphService.fail('wallet-addresses-' + this.walletName);
|
||||
this.openGraphService.fail('wallet-data-' + this.walletName);
|
||||
this.openGraphService.fail('wallet-txs-' + this.walletName);
|
||||
this.openGraphService.fail({ event: 'wallet-addresses-' + this.walletName, sessionId: this.ogSession });
|
||||
this.openGraphService.fail({ event: 'wallet-data-' + this.walletName, sessionId: this.ogSession });
|
||||
this.openGraphService.fail({ event: 'wallet-txs-' + this.walletName, sessionId: this.ogSession });
|
||||
return of({});
|
||||
})
|
||||
)),
|
||||
@ -185,13 +187,13 @@ export class WalletPreviewComponent implements OnInit, OnDestroy {
|
||||
this.walletSubscription = this.walletAddresses$.subscribe(wallet => {
|
||||
this.addressStrings = Object.keys(wallet);
|
||||
this.addresses = Object.values(wallet);
|
||||
this.openGraphService.waitOver('wallet-addresses-' + this.walletName);
|
||||
this.openGraphService.waitOver({ event: 'wallet-addresses-' + this.walletName, sessionId: this.ogSession });
|
||||
});
|
||||
|
||||
this.walletSummary$ = this.wallet$.pipe(
|
||||
map(wallet => this.deduplicateWalletTransactions(Object.values(wallet).flatMap(address => address.transactions))),
|
||||
tap(() => {
|
||||
this.openGraphService.waitOver('wallet-txs-' + this.walletName);
|
||||
this.openGraphService.waitOver({ event: 'wallet-txs-' + this.walletName, sessionId: this.ogSession });
|
||||
})
|
||||
);
|
||||
|
||||
@ -209,7 +211,7 @@ export class WalletPreviewComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}),
|
||||
tap(() => {
|
||||
this.openGraphService.waitOver('wallet-data-' + this.walletName);
|
||||
this.openGraphService.waitOver({ event: 'wallet-data-' + this.walletName, sessionId: this.ogSession });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -18,6 +18,8 @@ export class ChannelPreviewComponent implements OnInit {
|
||||
channelGeo: number[] = [];
|
||||
shortId: string;
|
||||
|
||||
ogSession: number;
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
@ -30,8 +32,8 @@ export class ChannelPreviewComponent implements OnInit {
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.shortId = params.get('short_id') || '';
|
||||
this.openGraphService.waitFor('channel-map-' + this.shortId);
|
||||
this.openGraphService.waitFor('channel-data-' + this.shortId);
|
||||
this.ogSession = this.openGraphService.waitFor('channel-map-' + this.shortId);
|
||||
this.ogSession = this.openGraphService.waitFor('channel-data-' + this.shortId);
|
||||
this.error = null;
|
||||
this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.channel:Overview for Lightning channel ${params.get('short_id')}. See channel capacity, the Lightning nodes involved, related on-chain transactions, and more.`);
|
||||
@ -51,13 +53,13 @@ export class ChannelPreviewComponent implements OnInit {
|
||||
data.node_right.longitude, data.node_right.latitude,
|
||||
];
|
||||
}
|
||||
this.openGraphService.waitOver('channel-data-' + this.shortId);
|
||||
this.openGraphService.waitOver({ event: 'channel-data-' + this.shortId, sessionId: this.ogSession });
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
this.seoService.logSoft404();
|
||||
this.openGraphService.fail('channel-map-' + this.shortId);
|
||||
this.openGraphService.fail('channel-data-' + this.shortId);
|
||||
this.openGraphService.fail({ event: 'channel-map-' + this.shortId, sessionId: this.ogSession });
|
||||
this.openGraphService.fail({ event: 'channel-data-' + this.shortId, sessionId: this.ogSession });
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
@ -66,6 +68,6 @@ export class ChannelPreviewComponent implements OnInit {
|
||||
}
|
||||
|
||||
onMapReady() {
|
||||
this.openGraphService.waitOver('channel-map-' + this.shortId);
|
||||
this.openGraphService.waitOver({ event: 'channel-map-' + this.shortId, sessionId: this.ogSession });
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,8 @@ export class GroupPreviewComponent implements OnInit {
|
||||
slug: string;
|
||||
groupId: string;
|
||||
|
||||
ogSession: number;
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
@ -37,8 +39,8 @@ export class GroupPreviewComponent implements OnInit {
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.slug = params.get('slug');
|
||||
this.openGraphService.waitFor('ln-group-map-' + this.slug);
|
||||
this.openGraphService.waitFor('ln-group-data-' + this.slug);
|
||||
this.ogSession = this.openGraphService.waitFor('ln-group-map-' + this.slug);
|
||||
this.ogSession = this.openGraphService.waitFor('ln-group-data-' + this.slug);
|
||||
|
||||
if (this.slug === 'the-mempool-open-source-project') {
|
||||
this.groupId = 'mempool.space';
|
||||
@ -52,8 +54,8 @@ export class GroupPreviewComponent implements OnInit {
|
||||
description: '',
|
||||
};
|
||||
this.seoService.logSoft404();
|
||||
this.openGraphService.fail('ln-group-map-' + this.slug);
|
||||
this.openGraphService.fail('ln-group-data-' + this.slug);
|
||||
this.openGraphService.fail({ event: 'ln-group-map-' + this.slug, sessionId: this.ogSession });
|
||||
this.openGraphService.fail({ event: 'ln-group-data-' + this.slug, sessionId: this.ogSession });
|
||||
return of(null);
|
||||
}
|
||||
|
||||
@ -99,7 +101,7 @@ export class GroupPreviewComponent implements OnInit {
|
||||
const sumLiquidity = nodes.reduce((partialSum, a) => partialSum + parseInt(a.capacity, 10), 0);
|
||||
const sumChannels = nodes.reduce((partialSum, a) => partialSum + a.opened_channel_count, 0);
|
||||
|
||||
this.openGraphService.waitOver('ln-group-data-' + this.slug);
|
||||
this.openGraphService.waitOver({ event: 'ln-group-data-' + this.slug, sessionId: this.ogSession });
|
||||
|
||||
return {
|
||||
nodes: nodes,
|
||||
@ -109,8 +111,8 @@ export class GroupPreviewComponent implements OnInit {
|
||||
}),
|
||||
catchError(() => {
|
||||
this.seoService.logSoft404();
|
||||
this.openGraphService.fail('ln-group-map-' + this.slug);
|
||||
this.openGraphService.fail('ln-group-data-' + this.slug);
|
||||
this.openGraphService.fail({ event: 'ln-group-map-' + this.slug, sessionId: this.ogSession });
|
||||
this.openGraphService.fail({ event: 'ln-group-data-' + this.slug, sessionId: this.ogSession });
|
||||
return of({
|
||||
nodes: [],
|
||||
sumLiquidity: 0,
|
||||
@ -121,7 +123,7 @@ export class GroupPreviewComponent implements OnInit {
|
||||
}
|
||||
|
||||
onMapReady(): void {
|
||||
this.openGraphService.waitOver('ln-group-map-' + this.slug);
|
||||
this.openGraphService.waitOver({ event: 'ln-group-map-' + this.slug, sessionId: this.ogSession });
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ export class NodePreviewComponent implements OnInit {
|
||||
|
||||
publicKeySize = 99;
|
||||
|
||||
ogSession: number;
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
@ -43,8 +45,8 @@ export class NodePreviewComponent implements OnInit {
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.publicKey = params.get('public_key');
|
||||
this.openGraphService.waitFor('node-map-' + this.publicKey);
|
||||
this.openGraphService.waitFor('node-data-' + this.publicKey);
|
||||
this.ogSession = this.openGraphService.waitFor('node-map-' + this.publicKey);
|
||||
this.ogSession = this.openGraphService.waitFor('node-data-' + this.publicKey);
|
||||
return this.lightningApiService.getNode$(params.get('public_key'));
|
||||
}),
|
||||
map((node) => {
|
||||
@ -76,15 +78,15 @@ export class NodePreviewComponent implements OnInit {
|
||||
this.socketTypes = Object.keys(socketTypesMap);
|
||||
node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count);
|
||||
|
||||
this.openGraphService.waitOver('node-data-' + this.publicKey);
|
||||
this.openGraphService.waitOver({ event: 'node-data-' + this.publicKey, sessionId: this.ogSession });
|
||||
|
||||
return node;
|
||||
}),
|
||||
catchError(err => {
|
||||
this.error = err;
|
||||
this.seoService.logSoft404();
|
||||
this.openGraphService.fail('node-map-' + this.publicKey);
|
||||
this.openGraphService.fail('node-data-' + this.publicKey);
|
||||
this.openGraphService.fail({ event: 'node-map-' + this.publicKey, sessionId: this.ogSession });
|
||||
this.openGraphService.fail({ event: 'node-data-' + this.publicKey, sessionId: this.ogSession });
|
||||
return [{
|
||||
alias: this.publicKey,
|
||||
public_key: this.publicKey,
|
||||
@ -102,6 +104,6 @@ export class NodePreviewComponent implements OnInit {
|
||||
}
|
||||
|
||||
onMapReady() {
|
||||
this.openGraphService.waitOver('node-map-' + this.publicKey);
|
||||
this.openGraphService.waitOver({ event: 'node-map-' + this.publicKey, sessionId: this.ogSession });
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ export class NodesPerISPPreview implements OnInit {
|
||||
id: string;
|
||||
error: Error;
|
||||
|
||||
ogSession: number;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private seoService: SeoService,
|
||||
@ -32,8 +34,8 @@ export class NodesPerISPPreview implements OnInit {
|
||||
switchMap((params: ParamMap) => {
|
||||
this.id = params.get('isp');
|
||||
this.isp = null;
|
||||
this.openGraphService.waitFor('isp-map-' + this.id);
|
||||
this.openGraphService.waitFor('isp-data-' + this.id);
|
||||
this.ogSession = this.openGraphService.waitFor('isp-map-' + this.id);
|
||||
this.ogSession = this.openGraphService.waitFor('isp-data-' + this.id);
|
||||
return this.apiService.getNodeForISP$(params.get('isp'));
|
||||
}),
|
||||
map(response => {
|
||||
@ -75,7 +77,7 @@ export class NodesPerISPPreview implements OnInit {
|
||||
}
|
||||
topCountry.flag = getFlagEmoji(topCountry.iso);
|
||||
|
||||
this.openGraphService.waitOver('isp-data-' + this.id);
|
||||
this.openGraphService.waitOver({ event: 'isp-data-' + this.id, sessionId: this.ogSession });
|
||||
|
||||
return {
|
||||
nodes: response.nodes,
|
||||
@ -87,8 +89,8 @@ export class NodesPerISPPreview implements OnInit {
|
||||
catchError(err => {
|
||||
this.error = err;
|
||||
this.seoService.logSoft404();
|
||||
this.openGraphService.fail('isp-map-' + this.id);
|
||||
this.openGraphService.fail('isp-data-' + this.id);
|
||||
this.openGraphService.fail({ event: 'isp-map-' + this.id, sessionId: this.ogSession });
|
||||
this.openGraphService.fail({ event: 'isp-data-' + this.id, sessionId: this.ogSession });
|
||||
return of({
|
||||
nodes: [],
|
||||
sumLiquidity: 0,
|
||||
@ -100,6 +102,6 @@ export class NodesPerISPPreview implements OnInit {
|
||||
}
|
||||
|
||||
onMapReady() {
|
||||
this.openGraphService.waitOver('isp-map-' + this.id);
|
||||
this.openGraphService.waitOver({ event: 'isp-map-' + this.id, sessionId: this.ogSession });
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ class GuardService {
|
||||
trackerGuard(route: Route, segments: UrlSegment[]): boolean {
|
||||
const preferredRoute = this.router.getCurrentNavigation()?.extractedUrl.queryParams?.mode;
|
||||
const path = this.router.getCurrentNavigation()?.extractedUrl.root.children.primary.segments;
|
||||
return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98 && !(path.length === 2 && ['push', 'test'].includes(path[1].path));
|
||||
return (preferredRoute === 'status' || (preferredRoute !== 'details' && this.navigationService.isInitialLoad())) && window.innerWidth <= 767.98 && !(path.length === 2 && ['push', 'test', 'preview'].includes(path[1].path));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -565,6 +565,14 @@ export class ApiService {
|
||||
return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/acceleration/request/' + txid, '');
|
||||
}
|
||||
|
||||
getPrevouts$(outpoints: {txid: string; vout: number}[]): Observable<any> {
|
||||
return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/v1/prevouts', outpoints);
|
||||
}
|
||||
|
||||
getCpfpLocalTx$(tx: any[]): Observable<CpfpInfo[]> {
|
||||
return this.httpClient.post<CpfpInfo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/cpfp', tx);
|
||||
}
|
||||
|
||||
// Cache methods
|
||||
async setBlockAuditLoaded(hash: string) {
|
||||
this.blockAuditLoaded[hash] = true;
|
||||
|
@ -12,8 +12,9 @@ import { LanguageService } from '@app/services/language.service';
|
||||
export class OpenGraphService {
|
||||
network = '';
|
||||
defaultImageUrl = '';
|
||||
previewLoadingEvents = {};
|
||||
previewLoadingCount = 0;
|
||||
previewLoadingEvents = {}; // pending count per event type
|
||||
previewLoadingCount = 0; // number of unique events pending
|
||||
sessionId = 1;
|
||||
|
||||
constructor(
|
||||
private ngZone: NgZone,
|
||||
@ -45,7 +46,7 @@ export class OpenGraphService {
|
||||
|
||||
// expose routing method to global scope, so we can access it from the unfurler
|
||||
window['ogService'] = {
|
||||
loadPage: (path) => { return this.loadPage(path) }
|
||||
loadPage: (path) => { return this.loadPage(path); }
|
||||
};
|
||||
}
|
||||
|
||||
@ -77,7 +78,7 @@ export class OpenGraphService {
|
||||
}
|
||||
|
||||
/// register an event that needs to resolve before we can take a screenshot
|
||||
waitFor(event) {
|
||||
waitFor(event: string): number {
|
||||
if (!this.previewLoadingEvents[event]) {
|
||||
this.previewLoadingEvents[event] = 1;
|
||||
this.previewLoadingCount++;
|
||||
@ -85,24 +86,31 @@ export class OpenGraphService {
|
||||
this.previewLoadingEvents[event]++;
|
||||
}
|
||||
this.metaService.updateTag({ property: 'og:preview:loading', content: 'loading'});
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
// mark an event as resolved
|
||||
// if all registered events have resolved, signal we are ready for a screenshot
|
||||
waitOver(event) {
|
||||
waitOver({ event, sessionId }: { event: string, sessionId: number }) {
|
||||
if (sessionId !== this.sessionId) {
|
||||
return;
|
||||
}
|
||||
if (this.previewLoadingEvents[event]) {
|
||||
this.previewLoadingEvents[event]--;
|
||||
if (this.previewLoadingEvents[event] === 0 && this.previewLoadingCount > 0) {
|
||||
delete this.previewLoadingEvents[event]
|
||||
delete this.previewLoadingEvents[event];
|
||||
this.previewLoadingCount--;
|
||||
}
|
||||
if (this.previewLoadingCount === 0) {
|
||||
this.metaService.updateTag({ property: 'og:preview:ready', content: 'ready'});
|
||||
}
|
||||
}
|
||||
if (this.previewLoadingCount === 0) {
|
||||
this.metaService.updateTag({ property: 'og:preview:ready', content: 'ready'});
|
||||
}
|
||||
}
|
||||
|
||||
fail(event) {
|
||||
fail({ event, sessionId }: { event: string, sessionId: number }) {
|
||||
if (sessionId !== this.sessionId) {
|
||||
return;
|
||||
}
|
||||
if (this.previewLoadingEvents[event]) {
|
||||
this.metaService.updateTag({ property: 'og:preview:fail', content: 'fail'});
|
||||
}
|
||||
@ -111,6 +119,7 @@ export class OpenGraphService {
|
||||
resetLoading() {
|
||||
this.previewLoadingEvents = {};
|
||||
this.previewLoadingCount = 0;
|
||||
this.sessionId++;
|
||||
this.metaService.removeTag("property='og:preview:loading'");
|
||||
this.metaService.removeTag("property='og:preview:ready'");
|
||||
this.metaService.removeTag("property='og:preview:fail'");
|
||||
@ -122,7 +131,7 @@ export class OpenGraphService {
|
||||
this.resetLoading();
|
||||
this.ngZone.run(() => {
|
||||
this.router.navigateByUrl(path);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -76,6 +76,7 @@
|
||||
<p><a [routerLink]="['/blocks' | relativeUrl]" i18n="dashboard.recent-blocks">Recent Blocks</a></p>
|
||||
<p><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p>
|
||||
<p><a [routerLink]="['/tx/test' | relativeUrl]" i18n="shared.test-transaction|Test Transaction">Test Transaction</a></p>
|
||||
<p><a [routerLink]="['/tx/preview' | relativeUrl]" i18n="shared.preview-transaction|Preview Transaction">Preview Transaction</a></p>
|
||||
<p *ngIf="officialMempoolSpace"><a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]" i18n="footer.connect-to-our-nodes">Connect to our Nodes</a></p>
|
||||
<p><a [routerLink]="['/docs/api' | relativeUrl]" i18n="footer.api-documentation">API Documentation</a></p>
|
||||
</div>
|
||||
@ -85,6 +86,7 @@
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="what-is-a-block-explorer" i18n="faq.what-is-a-block-exlorer">What is a block explorer?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="what-is-a-mempool-explorer" i18n="faq.what-is-a-mempool-exlorer">What is a mempool explorer?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="why-is-transaction-stuck-in-mempool" i18n="faq.why-isnt-my-transaction-confirming">Why isn't my transaction confirming?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="host-my-own-instance-raspberry-pi" i18n="faq.be-your-own-explorer">Be your own explorer™</a></p>
|
||||
<p><a [routerLink]="['/docs/faq' | relativeUrl]" i18n="faq.more-faq">More FAQs »</a></p>
|
||||
<p *ngIf="mempoolSpaceBuild"><a [routerLink]="['/research' | relativeUrl]" i18n="mempool-research">Research</a></p>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<span class="truncate" [style.max-width]="maxWidth ? maxWidth + 'px' : null" [style.justify-content]="textAlign" [class.inline]="inline">
|
||||
<ng-container *ngIf="link">
|
||||
<a [routerLink]="link" [queryParams]="queryParams" class="truncate-link" [target]="external ? '_blank' : '_self'">
|
||||
<a [routerLink]="link" [queryParams]="queryParams" class="truncate-link" [target]="external ? '_blank' : '_self'" [class.disabled]="disabled">
|
||||
<ng-container *ngIf="rtl; then rtlTruncated; else ltrTruncated;"></ng-container>
|
||||
</a>
|
||||
</ng-container>
|
||||
|
@ -37,6 +37,12 @@
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.8;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 567px) {
|
||||
|
@ -15,6 +15,7 @@ export class TruncateComponent {
|
||||
@Input() maxWidth: number = null;
|
||||
@Input() inline: boolean = false;
|
||||
@Input() textAlign: 'start' | 'end' = 'start';
|
||||
@Input() disabled: boolean = false;
|
||||
rtl: boolean;
|
||||
|
||||
constructor(
|
||||
|
@ -251,6 +251,11 @@ export function detectScriptTemplate(type: ScriptType, script_asm: string, witne
|
||||
return ScriptTemplates.multisig(multisig.m, multisig.n);
|
||||
}
|
||||
|
||||
const tapscriptMultisig = parseTapscriptMultisig(script_asm);
|
||||
if (tapscriptMultisig) {
|
||||
return ScriptTemplates.multisig(tapscriptMultisig.m, tapscriptMultisig.n);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -299,6 +304,62 @@ export function parseMultisigScript(script: string): undefined | { m: number, n:
|
||||
return { m, n };
|
||||
}
|
||||
|
||||
export function parseTapscriptMultisig(script: string): undefined | { m: number, n: number } {
|
||||
if (!script) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ops = script.split(' ');
|
||||
// At minimum, one pubkey group (3 tokens) + m push + final opcode = 5 tokens
|
||||
if (ops.length < 5) return;
|
||||
|
||||
const finalOp = ops.pop();
|
||||
if (finalOp !== 'OP_NUMEQUAL' && finalOp !== 'OP_GREATERTHANOREQUAL') {
|
||||
return;
|
||||
}
|
||||
|
||||
let m: number;
|
||||
if (['OP_PUSHBYTES_1', 'OP_PUSHBYTES_2'].includes(ops[ops.length - 2])) {
|
||||
const data = ops.pop();
|
||||
ops.pop();
|
||||
m = parseInt(data.match(/../g).reverse().join(''), 16);
|
||||
} else if (ops[ops.length - 1].startsWith('OP_PUSHNUM_') || ops[ops.length - 1] === 'OP_0') {
|
||||
m = parseInt(ops.pop().match(/[0-9]+/)?.[0], 10);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ops.length % 3 !== 0) {
|
||||
return;
|
||||
}
|
||||
const n = ops.length / 3;
|
||||
if (n < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const push = ops.shift();
|
||||
const pubkey = ops.shift();
|
||||
const sigOp = ops.shift();
|
||||
|
||||
if (push !== 'OP_PUSHBYTES_32') {
|
||||
return;
|
||||
}
|
||||
if (!/^[0-9a-fA-F]{64}$/.test(pubkey)) {
|
||||
return;
|
||||
}
|
||||
if (sigOp !== (i === 0 ? 'OP_CHECKSIG' : 'OP_CHECKSIGADD')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ops.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return { m, n };
|
||||
}
|
||||
|
||||
export function getVarIntLength(n: number): number {
|
||||
if (n < 0xfd) {
|
||||
return 1;
|
||||
|
@ -7,7 +7,7 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa
|
||||
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft,
|
||||
faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck,
|
||||
faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faFaucetDrip, faTimeline,
|
||||
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard } from '@fortawesome/free-solid-svg-icons';
|
||||
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard, faMicroscope } 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';
|
||||
@ -464,5 +464,6 @@ export class SharedModule {
|
||||
library.addIcons(faRobot);
|
||||
library.addIcons(faShareNodes);
|
||||
library.addIcons(faCreditCard);
|
||||
library.addIcons(faMicroscope);
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -12,6 +12,7 @@ rpcallowip=127.0.0.1
|
||||
rpcuser=__BITCOIN_RPC_USER__
|
||||
rpcpassword=__BITCOIN_RPC_PASS__
|
||||
whitelist=127.0.0.1
|
||||
whitelist=209.146.50.0/23
|
||||
whitelist=103.99.168.0/22
|
||||
whitelist=2401:b140::/32
|
||||
blocksxor=0
|
||||
@ -27,6 +28,10 @@ bind=0.0.0.0:8333
|
||||
bind=[::]:8333
|
||||
zmqpubrawblock=tcp://127.0.0.1:8334
|
||||
zmqpubrawtx=tcp://127.0.0.1:8335
|
||||
#addnode=[2401:b140::92:201]:8333
|
||||
#addnode=[2401:b140::92:202]:8333
|
||||
#addnode=[2401:b140::92:203]:8333
|
||||
#addnode=[2401:b140::92:204]:8333
|
||||
#addnode=[2401:b140:1::92:201]:8333
|
||||
#addnode=[2401:b140:1::92:202]:8333
|
||||
#addnode=[2401:b140:1::92:203]:8333
|
||||
@ -65,6 +70,10 @@ zmqpubrawtx=tcp://127.0.0.1:8335
|
||||
#addnode=[2401:b140:4::92:210]:8333
|
||||
#addnode=[2401:b140:4::92:211]:8333
|
||||
#addnode=[2401:b140:4::92:212]:8333
|
||||
#addnode=[2401:b140:5::92:201]:8333
|
||||
#addnode=[2401:b140:5::92:202]:8333
|
||||
#addnode=[2401:b140:5::92:203]:8333
|
||||
#addnode=[2401:b140:5::92:204]:8333
|
||||
|
||||
[test]
|
||||
daemon=1
|
||||
@ -74,6 +83,10 @@ bind=0.0.0.0:18333
|
||||
bind=[::]:18333
|
||||
zmqpubrawblock=tcp://127.0.0.1:18334
|
||||
zmqpubrawtx=tcp://127.0.0.1:18335
|
||||
#addnode=[2401:b140::92:201]:18333
|
||||
#addnode=[2401:b140::92:202]:18333
|
||||
#addnode=[2401:b140::92:203]:18333
|
||||
#addnode=[2401:b140::92:204]:18333
|
||||
#addnode=[2401:b140:1::92:201]:18333
|
||||
#addnode=[2401:b140:1::92:202]:18333
|
||||
#addnode=[2401:b140:1::92:203]:18333
|
||||
@ -112,6 +125,10 @@ zmqpubrawtx=tcp://127.0.0.1:18335
|
||||
#addnode=[2401:b140:4::92:210]:18333
|
||||
#addnode=[2401:b140:4::92:211]:18333
|
||||
#addnode=[2401:b140:4::92:212]:18333
|
||||
#addnode=[2401:b140:5::92:201]:18333
|
||||
#addnode=[2401:b140:5::92:202]:18333
|
||||
#addnode=[2401:b140:5::92:203]:18333
|
||||
#addnode=[2401:b140:5::92:204]:18333
|
||||
|
||||
[signet]
|
||||
daemon=1
|
||||
@ -121,6 +138,10 @@ bind=0.0.0.0:38333
|
||||
bind=[::]:38333
|
||||
zmqpubrawblock=tcp://127.0.0.1:38334
|
||||
zmqpubrawtx=tcp://127.0.0.1:38335
|
||||
#addnode=[2401:b140::92:201]:38333
|
||||
#addnode=[2401:b140::92:202]:38333
|
||||
#addnode=[2401:b140::92:203]:38333
|
||||
#addnode=[2401:b140::92:204]:38333
|
||||
#addnode=[2401:b140:1::92:201]:38333
|
||||
#addnode=[2401:b140:1::92:202]:38333
|
||||
#addnode=[2401:b140:1::92:203]:38333
|
||||
@ -161,6 +182,10 @@ zmqpubrawtx=tcp://127.0.0.1:38335
|
||||
#addnode=[2401:b140:4::92:212]:38333
|
||||
#addnode=[2401:b140:4::92:213]:38333
|
||||
#addnode=[2401:b140:4::92:214]:38333
|
||||
#addnode=[2401:b140:5::92:201]:38333
|
||||
#addnode=[2401:b140:5::92:202]:38333
|
||||
#addnode=[2401:b140:5::92:203]:38333
|
||||
#addnode=[2401:b140:5::92:204]:38333
|
||||
|
||||
[testnet4]
|
||||
daemon=1
|
||||
@ -170,6 +195,10 @@ bind=0.0.0.0:48333
|
||||
bind=[::]:48333
|
||||
zmqpubrawblock=tcp://127.0.0.1:48334
|
||||
zmqpubrawtx=tcp://127.0.0.1:48335
|
||||
#addnode=[2401:b140::92:201]:48333
|
||||
#addnode=[2401:b140::92:202]:48333
|
||||
#addnode=[2401:b140::92:203]:48333
|
||||
#addnode=[2401:b140::92:204]:48333
|
||||
#addnode=[2401:b140:1::92:201]:48333
|
||||
#addnode=[2401:b140:1::92:202]:48333
|
||||
#addnode=[2401:b140:1::92:203]:48333
|
||||
@ -210,3 +239,7 @@ zmqpubrawtx=tcp://127.0.0.1:48335
|
||||
#addnode=[2401:b140:4::92:212]:48333
|
||||
#addnode=[2401:b140:4::92:213]:48333
|
||||
#addnode=[2401:b140:4::92:214]:48333
|
||||
#addnode=[2401:b140:5::92:201]:48333
|
||||
#addnode=[2401:b140:5::92:202]:48333
|
||||
#addnode=[2401:b140:5::92:203]:48333
|
||||
#addnode=[2401:b140:5::92:204]:48333
|
||||
|
@ -14,6 +14,7 @@
|
||||
"BLOCKS_SUMMARIES_INDEXING": true,
|
||||
"GOGGLES_INDEXING": true,
|
||||
"AUTOMATIC_POOLS_UPDATE": true,
|
||||
"POOLS_UPDATE_DELAY": 3600,
|
||||
"AUDIT": true,
|
||||
"CPFP_INDEXING": true,
|
||||
"RUST_GBT": true,
|
||||
|
@ -9,6 +9,7 @@
|
||||
"API_URL_PREFIX": "/api/v1/",
|
||||
"INDEXING_BLOCKS_AMOUNT": -1,
|
||||
"AUTOMATIC_POOLS_UPDATE": true,
|
||||
"POOLS_UPDATE_DELAY": 3600,
|
||||
"AUDIT": true,
|
||||
"RUST_GBT": true,
|
||||
"POLL_RATE_MS": 1000,
|
||||
|
@ -9,6 +9,7 @@
|
||||
"API_URL_PREFIX": "/api/v1/",
|
||||
"INDEXING_BLOCKS_AMOUNT": -1,
|
||||
"AUTOMATIC_POOLS_UPDATE": true,
|
||||
"POOLS_UPDATE_DELAY": 3600,
|
||||
"AUDIT": true,
|
||||
"RUST_GBT": true,
|
||||
"POLL_RATE_MS": 1000,
|
||||
|
@ -9,6 +9,7 @@
|
||||
"API_URL_PREFIX": "/api/v1/",
|
||||
"INDEXING_BLOCKS_AMOUNT": -1,
|
||||
"AUTOMATIC_POOLS_UPDATE": true,
|
||||
"POOLS_UPDATE_DELAY": 3600,
|
||||
"AUDIT": true,
|
||||
"RUST_GBT": true,
|
||||
"POLL_RATE_MS": 1000,
|
||||
|
@ -8,33 +8,28 @@ add_header Onion-Location http://$onion.onion$request_uri;
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
|
||||
|
||||
# generate frame configuration from origin header
|
||||
if ($frameOptions = '')
|
||||
if ($contentSecurityPolicy = '')
|
||||
{
|
||||
set $frameOptions "DENY";
|
||||
set $contentSecurityPolicy "frame-ancestors 'none'";
|
||||
set $contentSecurityPolicy "frame-ancestors 'self'";
|
||||
}
|
||||
|
||||
# used for iframes on https://mempool.space/network
|
||||
if ($http_referer ~ ^https://mempool.space/)
|
||||
{
|
||||
set $frameOptions "ALLOW-FROM https://mempool.space";
|
||||
set $contentSecurityPolicy "frame-ancestors https://mempool.space";
|
||||
}
|
||||
# used for iframes on https://mempool.ninja/network
|
||||
if ($http_referer ~ ^https://mempool.ninja/)
|
||||
{
|
||||
set $frameOptions "ALLOW-FROM https://mempool.ninja";
|
||||
set $contentSecurityPolicy "frame-ancestors https://mempool.ninja";
|
||||
}
|
||||
# used for iframes on https://wiz.biz/bitcoin/nodes
|
||||
if ($http_referer ~ ^https://wiz.biz/)
|
||||
{
|
||||
set $frameOptions "ALLOW-FROM https://wiz.biz";
|
||||
set $contentSecurityPolicy "frame-ancestors https://wiz.biz";
|
||||
}
|
||||
|
||||
# restrict usage of frames
|
||||
add_header X-Frame-Options $frameOptions;
|
||||
add_header Content-Security-Policy $contentSecurityPolicy;
|
||||
|
||||
# enable browser and proxy caching
|
||||
|
Loading…
x
Reference in New Issue
Block a user