Merge branch 'master' into simon/fix-database-disabled

This commit is contained in:
Felipe Knorr Kuhn 2025-04-04 06:46:17 -07:00 committed by GitHub
commit fcebe001d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
85 changed files with 4050 additions and 436 deletions

View File

@ -3,16 +3,19 @@ name: CI Pipeline for the Backend and Frontend
on:
pull_request:
types: [opened, review_requested, synchronize]
push:
branches:
- master
jobs:
backend:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'"
strategy:
matrix:
node: ["20", "21"]
node: ["22"]
flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"
runs-on: ubuntu-latest
name: Backend (${{ matrix.flavor }}) - node ${{ matrix.node }}
steps:
@ -66,7 +69,7 @@ jobs:
cache:
name: "Cache assets for builds"
runs-on: "ubuntu-latest"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
@ -157,13 +160,13 @@ jobs:
frontend:
needs: cache
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'"
strategy:
matrix:
node: ["20", "21"]
node: ["22"]
flavor: ["dev", "prod"]
fail-fast: false
runs-on: "ubuntu-latest"
runs-on: ubuntu-latest
name: Frontend (${{ matrix.flavor }}) - node ${{ matrix.node }}
steps:
@ -245,8 +248,8 @@ jobs:
VERBOSE: 1
e2e:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
runs-on: "ubuntu-latest"
if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'"
runs-on: ubuntu-latest
needs: frontend
strategy:
fail-fast: false
@ -263,7 +266,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 20
node-version: 22
cache: "npm"
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json
@ -309,7 +312,7 @@ jobs:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:local-staging
start: npm run start:parameterized
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
@ -334,7 +337,7 @@ jobs:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:local-staging
start: npm run start:parameterized
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
@ -359,7 +362,7 @@ jobs:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:mempool
start: npm run start:local-staging
start: npm run start:parameterized
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
@ -375,10 +378,9 @@ jobs:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
validate_docker_json:
if: "!contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')"
runs-on: "ubuntu-latest"
if: "(github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ops') && !contains(github.head_ref, 'ops/')) || github.event_name == 'push'"
runs-on: ubuntu-latest
name: Validate generated backend Docker JSON
steps:

273
.github/workflows/e2e_parameterized.yml vendored Normal file
View File

@ -0,0 +1,273 @@
name: 'Parameterized e2e tests'
on:
workflow_dispatch:
inputs:
ref:
description: 'Branch name or Pull Request number (e.g., master or 6102)'
required: true
default: 'master'
type: string
mempool_hostname:
description: 'Mempool Hostname'
required: true
default: 'mempool.space'
type: string
liquid_hostname:
description: 'Liquid Hostname'
required: true
default: 'liquid.network'
type: string
jobs:
cache:
name: "Cache assets for builds"
runs-on: ubuntu-latest
steps:
- name: Determine checkout ref
id: determine-ref
run: |
REF_INPUT="${{ github.event.inputs.ref }}"
if [[ "$REF_INPUT" =~ ^[0-9]+$ ]]; then
echo "ref=refs/pull/$REF_INPUT/head" >> $GITHUB_OUTPUT
else
echo "ref=$REF_INPUT" >> $GITHUB_OUTPUT
fi
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ steps.determine-ref.outputs.ref }}
path: assets
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
registry-url: "https://registry.npmjs.org"
- name: Install (Prod dependencies only)
run: npm ci --omit=dev --omit=optional
working-directory: assets/frontend
- name: Restore cached mining pool assets
continue-on-error: true
id: cache-mining-pool-restore
uses: actions/cache/restore@v4
with:
path: |
mining-pool-assets.zip
key: mining-pool-assets-cache
- name: Restore promo video assets
continue-on-error: true
id: cache-promo-video-restore
uses: actions/cache/restore@v4
with:
path: |
promo-video-assets.zip
key: promo-video-assets-cache
- name: Unzip assets before building (src/resources)
continue-on-error: true
run: unzip -o mining-pool-assets.zip -d assets/frontend/src/resources/mining-pools
- name: Unzip assets before building (src/resources)
continue-on-error: true
run: unzip -o promo-video-assets.zip -d assets/frontend/src/resources/promo-video
# - name: Unzip assets before building (dist)
# continue-on-error: true
# run: unzip assets.zip -d assets/frontend/dist/mempool/browser/resources
- name: Sync-assets
run: npm run sync-assets-dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MEMPOOL_CDN: 1
VERBOSE: 1
working-directory: assets/frontend
- name: Zip mining-pool assets
run: zip -jrq mining-pool-assets.zip assets/frontend/src/resources/mining-pools/*
- name: Zip promo-video assets
run: zip -jrq promo-video-assets.zip assets/frontend/src/resources/promo-video/*
- name: Upload mining pool assets as artifact
uses: actions/upload-artifact@v4
with:
name: mining-pool-assets
path: mining-pool-assets.zip
- name: Upload promo video assets as artifact
uses: actions/upload-artifact@v4
with:
name: promo-video-assets
path: promo-video-assets.zip
- name: Save mining pool assets cache
id: cache-mining-pool-save
uses: actions/cache/save@v4
with:
path: |
mining-pool-assets.zip
key: mining-pool-assets-cache
- name: Save promo video assets cache
id: cache-promo-video-save
uses: actions/cache/save@v4
with:
path: |
promo-video-assets.zip
key: promo-video-assets-cache
e2e:
runs-on: ubuntu-latest
needs: cache
strategy:
fail-fast: false
matrix:
module: ["mempool", "liquid", "testnet4"]
name: E2E tests for ${{ matrix.module }}
steps:
- name: Determine checkout ref
id: determine-ref
run: |
REF_INPUT="${{ github.event.inputs.ref }}"
if [[ "$REF_INPUT" =~ ^[0-9]+$ ]]; then
echo "ref=refs/pull/$REF_INPUT/head" >> $GITHUB_OUTPUT
else
echo "ref=$REF_INPUT" >> $GITHUB_OUTPUT
fi
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ steps.determine-ref.outputs.ref }}
path: ${{ matrix.module }}
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 22
cache: "npm"
cache-dependency-path: ${{ matrix.module }}/frontend/package-lock.json
- name: Restore cached mining pool assets
continue-on-error: true
id: cache-mining-pool-restore
uses: actions/cache/restore@v4
with:
path: |
mining-pool-assets.zip
key: mining-pool-assets-cache
- name: Restore cached promo video assets
continue-on-error: true
id: cache-promo-video-restore
uses: actions/cache/restore@v4
with:
path: |
promo-video-assets.zip
key: promo-video-assets-cache
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: mining-pool-assets
- name: Unzip assets before building (src/resources)
run: unzip -o mining-pool-assets.zip -d ${{ matrix.module }}/frontend/src/resources/mining-pools
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: promo-video-assets
- name: Unzip assets before building (src/resources)
run: unzip -o promo-video-assets.zip -d ${{ matrix.module }}/frontend/src/resources/promo-video
# mempool
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'mempool' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:parameterized
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/mainnet/*.spec.ts
cypress/e2e/signet/*.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ref ${{ github.event.inputs.ref }} - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }}
MEMPOOL_HOSTNAME: ${{ github.event.inputs.mempool_hostname }}
LIQUID_HOSTNAME: ${{ github.event.inputs.liquid_hostname }}
MEMPOOL_CI_API_KEY: ${{ secrets.MEMPOOL_CI_API_KEY }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
# liquid
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'liquid' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:${{ matrix.module }}
start: npm run start:parameterized
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/liquid/liquid.spec.ts
cypress/e2e/liquidtestnet/liquidtestnet.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ref ${{ github.event.inputs.ref }} - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }}
MEMPOOL_HOSTNAME: ${{ github.event.inputs.mempool_hostname }}
LIQUID_HOSTNAME: ${{ github.event.inputs.liquid_hostname }}
MEMPOOL_CI_API_KEY: ${{ secrets.MEMPOOL_CI_API_KEY }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}
# testnet
- name: Chrome browser tests (${{ matrix.module }})
if: ${{ matrix.module == 'testnet4' }}
uses: cypress-io/github-action@v5
with:
tag: ${{ github.event_name }}
working-directory: ${{ matrix.module }}/frontend
build: npm run config:defaults:mempool
start: npm run start:parameterized
wait-on: "http://localhost:4200"
wait-on-timeout: 120
record: true
parallel: true
spec: |
cypress/e2e/testnet4/*.spec.ts
group: Tests on Chrome (${{ matrix.module }})
browser: "chrome"
ci-build-id: "${{ github.sha }}-${{ github.workflow }}-${{ github.event_name }}"
env:
COMMIT_INFO_MESSAGE: ${{ github.event_name }} (${{ github.sha }}) - ref ${{ github.event.inputs.ref }} - ${{ github.event.inputs.mempool_hostname }} - ${{ github.event.inputs.liquid_hostname }}
MEMPOOL_HOSTNAME: ${{ github.event.inputs.mempool_hostname }}
LIQUID_HOSTNAME: ${{ github.event.inputs.liquid_hostname }}
MEMPOOL_CI_API_KEY: ${{ secrets.MEMPOOL_CI_API_KEY }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_PROJECT_ID: ${{ secrets.CYPRESS_PROJECT_ID }}

View File

@ -4,7 +4,7 @@ on: [workflow_dispatch]
jobs:
print-backend-sha:
runs-on: 'ubuntu-latest'
runs-on: ubuntu-latest
name: Get block height
steps:
- name: Checkout

View File

@ -4,7 +4,7 @@ on: [workflow_dispatch]
jobs:
print-backend-sha:
runs-on: 'ubuntu-latest'
runs-on: ubuntu-latest
name: Print backend hashes
steps:
- name: Checkout

View File

@ -10,7 +10,7 @@ on:
type: string
jobs:
print-images-sha:
runs-on: 'ubuntu-latest'
runs-on: ubuntu-latest
name: Print digest for images
steps:
- name: Checkout

2
.nvmrc
View File

@ -1 +1 @@
v20.8.0
v22

View File

@ -10,7 +10,7 @@ However, this copyright license does not include an implied right or license
to use any trademarks, service marks, logos, or trade names of Mempool Space K.K.
or any other contributor to The Mempool Open Source Project.
The Mempool Open Source Project®, Mempool Accelerator, Mempool Enterprise®,
The Mempool Open Source Project®, Mempool Accelerator®, Mempool Enterprise®,
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo,
the mempool block visualization Logo, the mempool Blocks Logo, the mempool

View File

@ -406,8 +406,8 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block);
} catch (e) {
handleError(req, res, 500, 'Failed to get block');
} catch (e: any) {
handleError(req, res, e?.response?.status === 404 ? 404 : 500, 'Failed to get block');
}
}

View File

@ -1469,11 +1469,11 @@ class Blocks {
if (rows && Array.isArray(rows)) {
return rows.map(r => r.definition_hash);
} else {
logger.debug(`Unable to retreive list of blocks.definition_hash from db (no result)`);
logger.debug(`Unable to retrieve list of blocks.definition_hash from db (no result)`);
return null;
}
} catch (e) {
logger.debug(`Unable to retreive list of blocks.definition_hash from db (exception: ${e})`);
logger.debug(`Unable to retrieve list of blocks.definition_hash from db (exception: ${e})`);
return null;
}
}
@ -1484,11 +1484,11 @@ class Blocks {
if (rows && Array.isArray(rows)) {
return rows.map(r => r.hash);
} else {
logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (no result)`);
logger.debug(`Unable to retrieve list of blocks for definition hash ${definitionHash} from db (no result)`);
return null;
}
} catch (e) {
logger.debug(`Unable to retreive list of blocks for definition hash ${definitionHash} from db (exception: ${e})`);
logger.debug(`Unable to retrieve list of blocks for definition hash ${definitionHash} from db (exception: ${e})`);
return null;
}
}

View File

@ -30,6 +30,7 @@ const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes
class WalletApi {
private wallets: Record<string, Wallet> = {};
private syncing = false;
private lastSync = 0;
constructor() {
this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => {
@ -47,7 +48,38 @@ class WalletApi {
if (!config.WALLETS.ENABLED || this.syncing) {
return;
}
this.syncing = true;
if (config.WALLETS.AUTO && (Date.now() - this.lastSync) > POLL_FREQUENCY) {
try {
// update list of active wallets
this.lastSync = Date.now();
const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets`);
const walletList: string[] = response.data;
if (walletList) {
// create a quick lookup dictionary of active wallets
const newWallets: Record<string, boolean> = Object.fromEntries(
walletList.map(wallet => [wallet, true])
);
for (const wallet of walletList) {
// don't overwrite existing wallets
if (!this.wallets[wallet]) {
this.wallets[wallet] = { name: wallet, addresses: {}, lastPoll: 0 };
}
}
// remove wallets that are no longer active
for (const wallet of Object.keys(this.wallets)) {
if (!newWallets[wallet]) {
delete this.wallets[wallet];
}
}
}
} catch (e) {
logger.err(`Error updating active wallets: ${(e instanceof Error ? e.message : e)}`);
}
}
for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey];
if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) {
@ -72,6 +104,7 @@ class WalletApi {
}
}
}
this.syncing = false;
}

View File

@ -164,6 +164,7 @@ interface IConfig {
},
WALLETS: {
ENABLED: boolean;
AUTO: boolean;
WALLETS: string[];
},
STRATUM: {
@ -334,6 +335,7 @@ const defaults: IConfig = {
},
'WALLETS': {
'ENABLED': false,
'AUTO': false,
'WALLETS': [],
},
'STRATUM': {

View File

@ -131,6 +131,9 @@ class Server {
this.app
.use((req: Request, res: Response, next: NextFunction) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With');
res.setHeader('Access-Control-Expose-Headers', 'X-Total-Count,X-Mempool-Auth');
next();
})
.use(express.urlencoded({ extended: true }))

View File

@ -93,7 +93,7 @@ class HashratesRepository {
const [rows]: any[] = await DB.query(query);
return rows.map(row => row.timestamp);
} catch (e) {
logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
logger.err('Cannot retrieve indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
throw e;
}
}

View File

@ -261,20 +261,14 @@
"proxyConfig": "proxy.conf.mixed.js",
"verbose": true
},
"staging": {
"proxyConfig": "proxy.conf.js",
"disableHostCheck": true,
"host": "0.0.0.0",
"verbose": true
},
"local-prod": {
"proxyConfig": "proxy.conf.js",
"disableHostCheck": true,
"host": "0.0.0.0",
"verbose": false
},
"local-staging": {
"proxyConfig": "proxy.conf.staging.js",
"parameterized": {
"proxyConfig": "proxy.conf.parameterized.js",
"disableHostCheck": true,
"host": "0.0.0.0",
"verbose": false
@ -371,4 +365,4 @@
}
}
}
}
}

View File

@ -16,10 +16,10 @@
"mobileOrder": 4
},
{
"component": "balance",
"component": "walletBalance",
"mobileOrder": 1,
"props": {
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
"wallet": "ONBTC"
}
},
{
@ -30,21 +30,22 @@
}
},
{
"component": "address",
"component": "wallet",
"mobileOrder": 2,
"props": {
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo",
"period": "1m"
"wallet": "ONBTC",
"period": "1m",
"label": "bitcoin.gob.sv"
}
},
{
"component": "blocks"
},
{
"component": "addressTransactions",
"component": "walletTransactions",
"mobileOrder": 3,
"props": {
"address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo"
"wallet": "ONBTC"
}
}
]

View File

@ -57,11 +57,6 @@ describe('Liquid', () => {
});
});
it('loads the tv page - desktop', () => {
cy.visit(`${basePath}/tv`);
cy.waitForSkeletonGone();
});
it('loads the graphs page - mobile', () => {
cy.visit(`${basePath}`)
cy.waitForSkeletonGone();

View File

@ -57,11 +57,6 @@ describe('Liquid Testnet', () => {
cy.waitForSkeletonGone();
});
it('loads the tv page - desktop', () => {
cy.visit(`${basePath}/tv`);
cy.waitForSkeletonGone();
});
it('loads the graphs page - mobile', () => {
cy.visit(`${basePath}`)
cy.waitForSkeletonGone();

View File

@ -1,4 +1,4 @@
import { emitMempoolInfo, dropWebSocket } from '../../support/websocket';
import { emitMempoolInfo, dropWebSocket, receiveWebSocketMessageFromServer } from '../../support/websocket';
const baseModule = Cypress.env('BASE_MODULE');
@ -216,6 +216,69 @@ describe('Mainnet', () => {
cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').its('length').should('equal', 2);
cy.get('[data-cy="tx-1"] .table-tx-vout .highlight').invoke('text').should('contain', `${address}`);
});
describe('address poisoning', () => {
it('highlights potential address poisoning attacks on outputs, prefix and infix', () => {
const txid = '152a5dea805f95d6f83e50a9fd082630f542a52a076ebabdb295723eaf53fa30';
const prefix = '1DonatePLease';
const infix1 = 'SenderAddressXVXCmAY';
const infix2 = '5btcToSenderXXWBoKhB';
cy.visit(`/tx/${txid}`);
cy.waitForSkeletonGone();
cy.get('.alert-mempool').should('exist');
cy.get('.poison-alert').its('length').should('equal', 2);
cy.get('.prefix')
.should('have.length', 2)
.each(($el) => {
cy.wrap($el).should('have.text', prefix);
});
cy.get('.infix')
.should('have.length', 2)
.then(($infixes) => {
cy.wrap($infixes[0]).should('have.text', infix1);
cy.wrap($infixes[1]).should('have.text', infix2);
});
});
it('highlights potential address poisoning attacks on inputs and outputs, prefix, infix and postfix', () => {
const txid = '44544516084ea916ff1eb69c675c693e252addbbaf77102ffff86e3979ac6132';
const prefix = 'bc1qge8';
const infix1 = '6gqjnk8aqs3nvv7ejrvcd4zq6qur3';
const infix2 = 'xyxjex6zzzx5g8hh65vsel4e548p2';
const postfix1 = '6p6e3r';
const postfix2 = '6p6e3r';
cy.visit(`/tx/${txid}`);
cy.waitForSkeletonGone();
cy.get('.alert-mempool').should('exist');
cy.get('.poison-alert').its('length').should('equal', 2);
cy.get('.prefix')
.should('have.length', 2)
.each(($el) => {
cy.wrap($el).should('have.text', prefix);
});
cy.get('.infix')
.should('have.length', 2)
.then(($infixes) => {
cy.wrap($infixes[0]).should('have.text', infix1);
cy.wrap($infixes[1]).should('have.text', infix2);
});
cy.get('.postfix')
.should('have.length', 2)
.then(($postfixes) => {
cy.wrap($postfixes[0]).should('include.text', postfix1);
cy.wrap($postfixes[1]).should('include.text', postfix2);
});
});
});
});
describe('blocks navigation', () => {
@ -397,6 +460,7 @@ describe('Mainnet', () => {
cy.get('#dropdownFees').should('be.visible');
cy.get('.btn-group').should('be.visible');
});
it('check buttons - tablet', () => {
cy.viewport('ipad-2');
cy.visit('/graphs');
@ -405,6 +469,7 @@ describe('Mainnet', () => {
cy.get('#dropdownFees').should('be.visible');
cy.get('.btn-group').should('be.visible');
});
it('check buttons - desktop', () => {
cy.viewport('macbook-16');
cy.visit('/graphs');
@ -415,26 +480,6 @@ describe('Mainnet', () => {
});
});
it('loads the tv screen - desktop', () => {
cy.viewport('macbook-16');
cy.visit('/graphs/mempool');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.viewport('macbook-16');
cy.get('.chart-holder');
cy.get('.blockchain-wrapper').should('be.visible');
cy.get('#mempool-block-0').should('be.visible');
});
});
it('loads the tv screen - mobile', () => {
cy.viewport('iphone-6');
cy.visit('/tv');
cy.waitForSkeletonGone();
cy.get('.chart-holder');
cy.get('.blockchain-wrapper').should('not.visible');
});
it('loads the api screen', () => {
cy.visit('/');
cy.waitForSkeletonGone();
@ -516,7 +561,44 @@ describe('Mainnet', () => {
});
describe('RBF transactions', () => {
it('shows RBF transactions properly (mobile)', () => {
it('RBF page gets updated over websockets', () => {
cy.intercept('/api/v1/replacements', {
statusCode: 200,
body: []
});
cy.intercept('/api/v1/fullrbf/replacements', {
statusCode: 200,
body: []
});
cy.mockMempoolSocketV2();
cy.visit('/rbf');
cy.get('.no-replacements');
cy.get('.tree').should('have.length', 0);
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'rbf_page/rbf_01.json'
}
}
});
cy.get('.tree').should('have.length', 1);
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'rbf_page/rbf_02.json'
}
}
});
cy.get('.tree').should('have.length', 2);
});
it('shows RBF transactions properly (mobile - details)', () => {
cy.intercept('/api/v1/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f/cached', {
fixture: 'mainnet_tx_cached.json'
}).as('cached_tx');
@ -527,7 +609,7 @@ describe('Mainnet', () => {
cy.viewport('iphone-xr');
cy.mockMempoolSocket();
cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f');
cy.visit('/tx/21518a98d1aa9df524865d2f88c578499f524eb1d0c4d3e70312ab863508692f?mode=details');
cy.waitForSkeletonGone();
@ -545,7 +627,120 @@ describe('Mainnet', () => {
}
});
cy.get('.alert-mempool').should('be.visible');
});
it('shows RBF transactions properly (mobile - tracker)', () => {
cy.mockMempoolSocketV2();
cy.viewport('iphone-xr');
// API Mocks
cy.intercept('/api/v1/mining/pools/1w', {
fixture: 'details_rbf/api_mining_pools_1w.json'
}).as('api_mining_1w');
cy.intercept('/api/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29', {
statusCode: 404,
body: 'Transaction not found'
}).as('api_tx01_404');
cy.intercept('/api/v1/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29/cached', {
fixture: 'details_rbf/tx01_api_cached.json'
}).as('api_tx01_cached');
cy.intercept('/api/v1/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29/rbf', {
fixture: 'details_rbf/tx01_api_rbf.json'
}).as('api_tx01_rbf');
cy.visit('/tx/242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29?mode=tracker');
cy.wait('@api_tx01_rbf');
// Start sending mocked WS messages
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'details_rbf/tx01_ws_stratum_jobs.json'
}
}
});
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'details_rbf/tx01_ws_blocks_01.json'
}
}
});
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'details_rbf/tx01_ws_tx_replaced.json'
}
}
});
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'details_rbf/tx01_ws_mempool_blocks_01.json'
}
}
});
cy.get('.alert-replaced').should('be.visible');
cy.get('.explainer').should('be.visible');
cy.get('svg[data-icon=timeline]').should('be.visible');
// Second TX setup
cy.intercept('/api/tx/b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698', {
fixture: 'details_rbf/tx02_api_tx.json'
}).as('tx02_api');
cy.intercept('/api/v1/transaction-times?txId%5B%5D=b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698', {
fixture: 'details_rbf/tx02_api_tx_times.json'
}).as('tx02_api_tx_times');
cy.intercept('/api/v1/tx/b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698/rbf', {
fixture: 'details_rbf/tx02_api_rbf.json'
}).as('tx02_api_rbf');
cy.intercept('/api/v1/cpfp/b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698', {
fixture: 'details_rbf/tx02_api_cpfp.json'
}).as('tx02_api_cpfp');
// Go to the replacement tx
cy.get('.alert-replaced a').click();
cy.wait('@tx02_api_cpfp');
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'details_rbf/tx02_ws_tx_position.json'
}
}
});
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'details_rbf/tx02_ws_mempool_blocks_01.json'
}
}
});
cy.get('svg[data-icon=hourglass-half]').should('be.visible');
receiveWebSocketMessageFromServer({
params: {
file: {
path: 'details_rbf/tx02_ws_block.json'
}
}
});
cy.get('app-confirmations');
cy.get('svg[data-icon=circle-check]').should('be.visible');
});
it('shows RBF transactions properly (desktop)', () => {

View File

@ -60,30 +60,6 @@ describe('Signet', () => {
});
});
describe.skip('tv mode', () => {
it('loads the tv screen - desktop', () => {
cy.viewport('macbook-16');
cy.visit('/signet/graphs');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.get('.chart-holder').should('be.visible');
cy.get('#mempool-block-0').should('be.visible');
cy.get('.tv-only').should('not.exist');
});
});
it('loads the tv screen - mobile', () => {
cy.visit('/signet/graphs');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.viewport('iphone-8');
cy.get('.chart-holder').should('be.visible');
cy.get('.tv-only').should('not.exist');
cy.get('#mempool-block-0').should('be.visible');
});
});
});
it('loads the api screen', () => {
cy.visit('/signet');
cy.waitForSkeletonGone();

View File

@ -60,30 +60,6 @@ describe('Testnet4', () => {
});
});
describe('tv mode', () => {
it('loads the tv screen - desktop', () => {
cy.viewport('macbook-16');
cy.visit('/testnet4/graphs');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.wait(1000);
cy.get('.tv-only').should('not.exist');
cy.get('#mempool-block-0').should('be.visible');
});
});
it('loads the tv screen - mobile', () => {
cy.visit('/testnet4/graphs');
cy.waitForSkeletonGone();
cy.get('#btn-tv').click().then(() => {
cy.viewport('iphone-6');
cy.wait(1000);
cy.get('.tv-only').should('not.exist');
});
});
});
it('loads the api screen', () => {
cy.visit('/testnet4');
cy.waitForSkeletonGone();

View File

@ -0,0 +1,60 @@
{
"txSummary": {
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698",
"effectiveVsize": 224,
"effectiveFee": 960,
"ancestorCount": 1
},
"cost": 1000,
"targetFeeRate": 3,
"nextBlockFee": 672,
"userBalance": 0,
"mempoolBaseFee": 50000,
"vsizeFee": 0,
"pools": [
36,
102,
112,
44,
4,
2,
6,
94,
143,
43,
105,
115,
142,
111
],
"options": [
{
"fee": 1000
},
{
"fee": 2000
},
{
"fee": 10000
}
],
"hasAccess": false,
"availablePaymentMethods": {
"bitcoin": {
"enabled": true,
"min": 1000,
"max": 10000000
},
"applePay": {
"enabled": true,
"min": 10,
"max": 1000
},
"googlePay": {
"enabled": true,
"min": 10,
"max": 1000
}
},
"unavailable": false
}

View File

@ -0,0 +1,3 @@
{
"gitCommit": "62f80296"
}

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1,260 @@
{
"pools": [
{
"poolId": 112,
"name": "Foundry USA",
"link": "https://foundrydigital.com",
"blockCount": 323,
"rank": 1,
"emptyBlocks": 0,
"slug": "foundryusa",
"avgMatchRate": 99.96,
"avgFeeDelta": "-0.01971455",
"poolUniqueId": 111
},
{
"poolId": 45,
"name": "AntPool",
"link": "https://www.antpool.com",
"blockCount": 171,
"rank": 2,
"emptyBlocks": 0,
"slug": "antpool",
"avgMatchRate": 99.99,
"avgFeeDelta": "-0.04227368",
"poolUniqueId": 44
},
{
"poolId": 74,
"name": "ViaBTC",
"link": "https://viabtc.com",
"blockCount": 166,
"rank": 3,
"emptyBlocks": 0,
"slug": "viabtc",
"avgMatchRate": 99.99,
"avgFeeDelta": "-0.02530964",
"poolUniqueId": 73
},
{
"poolId": 37,
"name": "F2Pool",
"link": "https://www.f2pool.com",
"blockCount": 104,
"rank": 4,
"emptyBlocks": 0,
"slug": "f2pool",
"avgMatchRate": 99.99,
"avgFeeDelta": "-0.03299327",
"poolUniqueId": 36
},
{
"poolId": 116,
"name": "MARA Pool",
"link": "https://marapool.com",
"blockCount": 66,
"rank": 5,
"emptyBlocks": 0,
"slug": "marapool",
"avgMatchRate": 99.97,
"avgFeeDelta": "0.02366061",
"poolUniqueId": 115
},
{
"poolId": 103,
"name": "SpiderPool",
"link": "https://www.spiderpool.com",
"blockCount": 46,
"rank": 6,
"emptyBlocks": 1,
"slug": "spiderpool",
"avgMatchRate": 97.82,
"avgFeeDelta": "-0.07258913",
"poolUniqueId": 102
},
{
"poolId": 142,
"name": "SECPOOL",
"link": "https://www.secpool.com",
"blockCount": 30,
"rank": 7,
"emptyBlocks": 1,
"slug": "secpool",
"avgMatchRate": 96.67,
"avgFeeDelta": "-0.06596000",
"poolUniqueId": 141
},
{
"poolId": 106,
"name": "Binance Pool",
"link": "https://pool.binance.com",
"blockCount": 28,
"rank": 8,
"emptyBlocks": 0,
"slug": "binancepool",
"avgMatchRate": 99.99,
"avgFeeDelta": "-0.05834286",
"poolUniqueId": 105
},
{
"poolId": 5,
"name": "Luxor",
"link": "https://mining.luxor.tech",
"blockCount": 28,
"rank": 9,
"emptyBlocks": 0,
"slug": "luxor",
"avgMatchRate": 100,
"avgFeeDelta": "-0.05496071",
"poolUniqueId": 4
},
{
"poolId": 143,
"name": "OCEAN",
"link": "https://ocean.xyz/",
"blockCount": 12,
"rank": 10,
"emptyBlocks": 0,
"slug": "ocean",
"avgMatchRate": 91.9,
"avgFeeDelta": "-0.14650833",
"poolUniqueId": 142
},
{
"poolId": 44,
"name": "Braiins Pool",
"link": "https://braiins.com/pool",
"blockCount": 12,
"rank": 11,
"emptyBlocks": 0,
"slug": "braiinspool",
"avgMatchRate": 100,
"avgFeeDelta": "-0.03553333",
"poolUniqueId": 43
},
{
"poolId": 113,
"name": "SBI Crypto",
"link": "https://sbicrypto.com",
"blockCount": 8,
"rank": 12,
"emptyBlocks": 0,
"slug": "sbicrypto",
"avgMatchRate": 98.65,
"avgFeeDelta": "-0.04246250",
"poolUniqueId": 112
},
{
"poolId": 152,
"name": "Carbon Negative",
"link": "https://github.com/bitcoin-data/mining-pools/issues/48",
"blockCount": 7,
"rank": 13,
"emptyBlocks": 0,
"slug": "carbonnegative",
"avgMatchRate": 99.75,
"avgFeeDelta": "-0.04407143",
"poolUniqueId": 151
},
{
"poolId": 7,
"name": "BTC.com",
"link": "https://pool.btc.com",
"blockCount": 5,
"rank": 14,
"emptyBlocks": 0,
"slug": "btccom",
"avgMatchRate": 99.98,
"avgFeeDelta": "-0.02496000",
"poolUniqueId": 6
},
{
"poolId": 162,
"name": "Mining Squared",
"link": "https://pool.bsquared.network/",
"blockCount": 4,
"rank": 15,
"emptyBlocks": 0,
"slug": "miningsquared",
"avgMatchRate": 100,
"avgFeeDelta": "-0.00915000",
"poolUniqueId": 161
},
{
"poolId": 95,
"name": "Poolin",
"link": "https://www.poolin.com",
"blockCount": 4,
"rank": 16,
"emptyBlocks": 0,
"slug": "poolin",
"avgMatchRate": 100,
"avgFeeDelta": "-0.26485000",
"poolUniqueId": 94
},
{
"poolId": 1,
"name": "Unknown",
"link": "https://learnmeabitcoin.com/technical/coinbase-transaction",
"blockCount": 4,
"rank": 17,
"emptyBlocks": 0,
"slug": "unknown",
"avgMatchRate": 100,
"avgFeeDelta": "-0.06490000",
"poolUniqueId": 0
},
{
"poolId": 144,
"name": "WhitePool",
"link": "https://whitebit.com/mining-pool",
"blockCount": 3,
"rank": 18,
"emptyBlocks": 0,
"slug": "whitepool",
"avgMatchRate": 100,
"avgFeeDelta": "-0.01293333",
"poolUniqueId": 143
},
{
"poolId": 3,
"name": "ULTIMUSPOOL",
"link": "https://www.ultimuspool.com",
"blockCount": 1,
"rank": 19,
"emptyBlocks": 0,
"slug": "ultimuspool",
"avgMatchRate": 100,
"avgFeeDelta": "-0.16130000",
"poolUniqueId": 2
},
{
"poolId": 50,
"name": "Solo CK",
"link": "https://solo.ckpool.org",
"blockCount": 1,
"rank": 20,
"emptyBlocks": 0,
"slug": "solock",
"avgMatchRate": 100,
"avgFeeDelta": "-0.01510000",
"poolUniqueId": 49
},
{
"poolId": 158,
"name": "BitFuFuPool",
"link": "https://www.bitfufu.com/pool",
"blockCount": 1,
"rank": 21,
"emptyBlocks": 0,
"slug": "bitfufupool",
"avgMatchRate": 100,
"avgFeeDelta": "-0.01630000",
"poolUniqueId": 157
}
],
"blockCount": 1024,
"lastEstimatedHashrate": 786391245138648900000,
"lastEstimatedHashrate3d": 797683179385121300000,
"lastEstimatedHashrate1w": 827836055441520300000
}

View File

@ -0,0 +1,55 @@
{
"txid": "242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29",
"version": 2,
"locktime": 0,
"vin": [
{
"txid": "fb16c141d22a3a7af5e44e7478a8663866c35d554cd85107ec8a99b97e5a72e9",
"vout": 0,
"prevout": {
"scriptpubkey": "76a914099f831c49289c7b9cc07a9a632867ecd51a105a88ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 099f831c49289c7b9cc07a9a632867ecd51a105a OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1stAprEZapjCYGUACUcXohQqSHF9MU5Kj",
"value": 50000
},
"scriptsig": "483045022100f44a50e894c30b28400933da120b872ff15bd2e66dd034ffbbb925fd054dd60f02206ba477a9da3fae68a9f420f53bc25493270bd987d13b75fef39cf514aea9cdb3014104ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df",
"scriptsig_asm": "OP_PUSHBYTES_72 3045022100f44a50e894c30b28400933da120b872ff15bd2e66dd034ffbbb925fd054dd60f02206ba477a9da3fae68a9f420f53bc25493270bd987d13b75fef39cf514aea9cdb301 OP_PUSHBYTES_65 04ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df",
"is_coinbase": false,
"sequence": 4294967293
}
],
"vout": [
{
"scriptpubkey": "5120a2fc5cb445755389eed295ab9d594b0facd3e00840a3e477fa7c025412c53795",
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 a2fc5cb445755389eed295ab9d594b0facd3e00840a3e477fa7c025412c53795",
"scriptpubkey_type": "v1_p2tr",
"scriptpubkey_address": "bc1p5t79edz9w4fcnmkjjk4e6k2tp7kd8cqggz37gal60sp9gyk9x72sk4mk0f",
"value": 49394
}
],
"size": 233,
"weight": 932,
"sigops": 0,
"fee": 606,
"status": {
"confirmed": false
},
"order": 701313494,
"vsize": 233,
"adjustedVsize": 233,
"feePerVsize": 2.6008583690987126,
"adjustedFeePerVsize": 2.6008583690987126,
"effectiveFeePerVsize": 2.6008583690987126,
"firstSeen": 1743541407,
"inputs": [],
"cpfpDirty": false,
"ancestors": [],
"descendants": [],
"bestDescendant": null,
"position": {
"block": 0,
"vsize": 318595.5
},
"flags": 1099511645193
}

View File

@ -0,0 +1,34 @@
{
"replacements": {
"tx": {
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698",
"fee": 960,
"vsize": 224,
"value": 49040,
"rate": 4.285714285714286,
"time": 1743541726,
"rbf": true,
"fullRbf": false
},
"time": 1743541726,
"fullRbf": false,
"replaces": [
{
"tx": {
"txid": "242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29",
"fee": 606,
"vsize": 233,
"value": 49394,
"rate": 2.6008583690987126,
"time": 1743541407,
"rbf": true
},
"time": 1743541407,
"interval": 319,
"fullRbf": false,
"replaces": []
}
]
},
"replaces": null
}

View File

@ -0,0 +1,579 @@
{
"blocks": [
{
"id": "0000000000000000000079f5b74b6533abb0b1ece06570d8d157b5bebd1460b4",
"height": 890440,
"version": 559235072,
"timestamp": 1743535677,
"bits": 386038124,
"nonce": 2920325684,
"difficulty": 113757508810854,
"merkle_root": "c793d5fdbfb1ebe99e14a13a6d65370057d311774d33c71da166663b18722474",
"tx_count": 3823,
"size": 1578209,
"weight": 3993461,
"previousblockhash": "000000000000000000020fb2e24425793e17e60e188205dc1694d221790348b2",
"mediantime": 1743532406,
"stale": false,
"extras": {
"reward": 319838750,
"coinbaseRaw": "0348960d082f5669614254432f2cfabe6d6d294719da11c017243828bf32c405341db7f19387fee92c25413c45e114907f9810000000000000001058bf9601429f9fa7a6c160d10d00000000000000",
"orphans": [],
"medianFee": 4,
"feeRange": [
3,
3,
3.0191082802547773,
3.980952380952381,
5,
10,
427.748502994012
],
"totalFees": 7338750,
"avgFee": 1920,
"avgFeeRate": 7,
"utxoSetChange": 4093,
"avgTxSize": 412.71000000000004,
"totalInputs": 7430,
"totalOutputs": 11523,
"totalOutputAmt": 547553568373,
"segwitTotalTxs": 3432,
"segwitTotalSize": 1467920,
"segwitTotalWeight": 3552413,
"feePercentiles": null,
"virtualSize": 998365.25,
"coinbaseAddress": "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4",
"coinbaseAddresses": [
"1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4"
],
"coinbaseSignature": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb37342f6275b13936799def06f2eb4c0f201515 OP_EQUALVERIFY OP_CHECKSIG",
"coinbaseSignatureAscii": "\u0003H–\r\b/ViaBTC/,ú¾mm)G\u0019Ú\u0011À\u0017$8(¿2Ä\u00054\u001d·ñ“‡þé,%A<Eá\u0014˜\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0010X¿–\u0001BŸŸ§¦Á`Ñ\r\u0000\u0000\u0000\u0000\u0000\u0000\u0000",
"header": "00405521b248037921d29416dc0582180ee6173e792544e2b20f02000000000000000000742472183b6666a11dc7334d7711d3570037656d3aa1149ee9ebb1bffdd593c73d3eec676c79021734a210ae",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 73,
"name": "ViaBTC",
"slug": "viabtc",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 7342711,
"expectedWeight": 3991920,
"similarity": 0.9978345275528634
}
},
{
"id": "00000000000000000000ef5a27459785ea4c91e05f64adfad306af6dfc0cd19c",
"height": 890441,
"version": 540188672,
"timestamp": 1743535803,
"bits": 386038124,
"nonce": 3418993591,
"difficulty": 113757508810854,
"merkle_root": "25457e2f9e9b55cacde6166acc58ebc2367007a7af5d9b39f46dc1ce060fd63e",
"tx_count": 2813,
"size": 1671284,
"weight": 3993398,
"previousblockhash": "0000000000000000000079f5b74b6533abb0b1ece06570d8d157b5bebd1460b4",
"mediantime": 1743532471,
"stale": false,
"extras": {
"reward": 315271317,
"coinbaseRaw": "0349960d04bb3eec672f466f756e6472792055534120506f6f6c202364726f70676f6c642f01530a4561e7020000000000",
"orphans": [],
"medianFee": 2.0116279069767438,
"feeRange": [
1.0567375886524824,
1.1917098445595855,
1.9007678676904902,
2.1298076923076925,
2.825034262220192,
3.175202156334232,
121
],
"totalFees": 2771317,
"avgFee": 985,
"avgFeeRate": 2,
"utxoSetChange": 37,
"avgTxSize": 593.97,
"totalInputs": 8679,
"totalOutputs": 8716,
"totalOutputAmt": 299780221694,
"segwitTotalTxs": 2379,
"segwitTotalSize": 1535232,
"segwitTotalWeight": 3449298,
"feePercentiles": null,
"virtualSize": 998349.5,
"coinbaseAddress": "bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63",
"coinbaseAddresses": [
"bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63",
"bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38"
],
"coinbaseSignature": "OP_PUSHNUM_1 OP_PUSHBYTES_32 0f9dab1a72f7c48da8a1df2f913bef649bfc0d77072dffd11329b8048293d7a3",
"coinbaseSignatureAscii": "\u0003I–\r\u0004»>ìg/Foundry USA Pool #dropgold/\u0001S\nEaç\u0002\u0000\u0000\u0000\u0000\u0000",
"header": "00a03220b46014bdbeb557d1d87065e0ecb1b0ab33654bb7f579000000000000000000003ed60f06cec16df4399b5dafa7077036c2eb58cc6a16e6cdca559b9e2f7e4525bb3eec676c790217b7b3c9cb",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 111,
"name": "Foundry USA",
"slug": "foundryusa",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 2792968,
"expectedWeight": 3991959,
"similarity": 0.9951416839808291
}
},
{
"id": "000000000000000000014845978a876b3d8bf5d489b9d87e88873952b06ddfce",
"height": 890442,
"version": 557981696,
"timestamp": 1743536834,
"bits": 386038124,
"nonce": 470697326,
"difficulty": 113757508810854,
"merkle_root": "5e92e681c1db2797a5b3e5016729059f8b60a256cafb51d835dac2b3964c0db4",
"tx_count": 3566,
"size": 1628328,
"weight": 3993552,
"previousblockhash": "00000000000000000000ef5a27459785ea4c91e05f64adfad306af6dfc0cd19c",
"mediantime": 1743532867,
"stale": false,
"extras": {
"reward": 318057766,
"coinbaseRaw": "034a960d194d696e656420627920416e74506f6f6c204d000201e15e2989fabe6d6dd599e9dfa40be51f1517c8f512c5c3d51c7656182f1df335d34b98ee02c527db080000000000000000004f92b702000000000000",
"orphans": [],
"medianFee": 3.00860164711668,
"feeRange": [
1.5174418604651163,
2.0140845070422535,
2.492354740061162,
3,
4.020942408376963,
7,
200
],
"totalFees": 5557766,
"avgFee": 1558,
"avgFeeRate": 5,
"utxoSetChange": 1971,
"avgTxSize": 456.48,
"totalInputs": 7938,
"totalOutputs": 9909,
"totalOutputAmt": 900044492230,
"segwitTotalTxs": 3214,
"segwitTotalSize": 1526463,
"segwitTotalWeight": 3586200,
"feePercentiles": null,
"virtualSize": 998388,
"coinbaseAddress": "37jKPSmbEGwgfacCr2nayn1wTaqMAbA94Z",
"coinbaseAddresses": [
"37jKPSmbEGwgfacCr2nayn1wTaqMAbA94Z",
"39C7fxSzEACPjM78Z7xdPxhf7mKxJwvfMJ"
],
"coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 42402a28dd61f2718a4b27ae72a4791d5bbdade7 OP_EQUAL",
"coinbaseSignatureAscii": "\u0003J–\r\u0019Mined by AntPool M\u0000\u0002\u0001á^)‰ú¾mmՙéߤ\u000bå\u001f\u0015\u0017Èõ\u0012ÅÃÕ\u001cvV\u0018/\u001dó5ÓK˜î\u0002Å'Û\b\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000O’·\u0002\u0000\u0000\u0000\u0000\u0000\u0000",
"header": "002042219cd10cfc6daf06d3faad645fe0914cea859745275aef00000000000000000000b40d4c96b3c2da35d851fbca56a2608b9f05296701e5b3a59727dbc181e6925ec242ec676c7902176e450e1c",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 44,
"name": "AntPool",
"slug": "antpool",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 5764747,
"expectedWeight": 3991786,
"similarity": 0.9029319155137951
}
},
{
"id": "000000000000000000026e08b270834273511b353bed30d54706211adc96f5f6",
"height": 890443,
"version": 706666496,
"timestamp": 1743537197,
"bits": 386038124,
"nonce": 321696065,
"difficulty": 113757508810854,
"merkle_root": "3d7574f7eca741fa94b4690868a242e5b286f8a0417ad0275d4ab05893e96350",
"tx_count": 2155,
"size": 1700002,
"weight": 3993715,
"previousblockhash": "000000000000000000014845978a876b3d8bf5d489b9d87e88873952b06ddfce",
"mediantime": 1743533789,
"stale": false,
"extras": {
"reward": 315112344,
"coinbaseRaw": "034b960d21202020204d696e656420627920536563706f6f6c2020202070000b05e388958c01fabe6d6db7ae4bfa7b1294e16e800b4563f1f5ddeb5c0740319eba45600f3f05d2d7272910000000000000000000c2cb7e020000",
"orphans": [],
"medianFee": 1.4360674424569184,
"feeRange": [
1,
1.0135135135135136,
1.09717868338558,
2.142857142857143,
3.009584664536741,
4.831858407079646,
196.07843137254903
],
"totalFees": 2612344,
"avgFee": 1212,
"avgFeeRate": 2,
"utxoSetChange": -2880,
"avgTxSize": 788.64,
"totalInputs": 9773,
"totalOutputs": 6893,
"totalOutputAmt": 264603969671,
"segwitTotalTxs": 1933,
"segwitTotalSize": 1556223,
"segwitTotalWeight": 3418707,
"feePercentiles": null,
"virtualSize": 998428.75,
"coinbaseAddress": "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9",
"coinbaseAddresses": [
"3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9",
"3Awm3FNpmwrbvAFVThRUFqgpbVuqWisni9"
],
"coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 8ee90177614ecde53314fd67c46162f315852a07 OP_EQUAL",
"coinbaseSignatureAscii": "\u0003K–\r! Mined by Secpool p\u0000\u000b\u0005㈕Œ\u0001ú¾mm·®Kú{\u0012”án€\u000bEcñõÝë\\\u0007@1žºE`\u000f?\u0005Ò×')\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000ÂË~\u0002\u0000\u0000",
"header": "00e01e2acedf6db0523987887ed8b989d4f58b3d6b878a974548010000000000000000005063e99358b04a5d27d07a41a0f886b2e542a2680869b494fa41a7ecf774753d2d44ec676c79021741b12c13",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 141,
"name": "SECPOOL",
"slug": "secpool",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 2623934,
"expectedWeight": 3991917,
"similarity": 0.9951244468050102
}
},
{
"id": "00000000000000000001b36ec470ae4ec39f5d975f665d6f40e9d35bfa65290d",
"height": 890444,
"version": 671080448,
"timestamp": 1743539347,
"bits": 386038124,
"nonce": 994357124,
"difficulty": 113757508810854,
"merkle_root": "c891d4bf68e22916274b667eb3287d50da2ddd63f8dad892da045cc2ad4a7b21",
"tx_count": 3797,
"size": 1500309,
"weight": 3993525,
"previousblockhash": "000000000000000000026e08b270834273511b353bed30d54706211adc96f5f6",
"mediantime": 1743533986,
"stale": false,
"extras": {
"reward": 318708524,
"coinbaseRaw": "034c960d082f5669614254432f2cfabe6d6d45b7fd7ab53a0914da7dcc9d21fe44f0936f5354169a56df9d5139f07afbc2b41000000000000000106fc0eb03f0ac2e851d18d8d9f85ad70000000000",
"orphans": [],
"medianFee": 4.064775540157046,
"feeRange": [
3.014354066985646,
3.18368700265252,
3.602836879432624,
4.231825525040388,
5.581730769230769,
10,
697.7151162790698
],
"totalFees": 6208524,
"avgFee": 1635,
"avgFeeRate": 6,
"utxoSetChange": 5755,
"avgTxSize": 395.02,
"totalInputs": 6681,
"totalOutputs": 12436,
"totalOutputAmt": 835839828101,
"segwitTotalTxs": 3351,
"segwitTotalSize": 1354446,
"segwitTotalWeight": 3410181,
"feePercentiles": null,
"virtualSize": 998381.25,
"coinbaseAddress": "1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4",
"coinbaseAddresses": [
"1PuJjnF476W3zXfVYmJfGnouzFDAXakkL4"
],
"coinbaseSignature": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb37342f6275b13936799def06f2eb4c0f201515 OP_EQUALVERIFY OP_CHECKSIG",
"coinbaseSignatureAscii": "\u0003L–\r\b/ViaBTC/,ú¾mmE·ýzµ:\t\u0014Ú}̝!þDð“oST\u0016šQ9ðzû´\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0010oÀë\u0003ð¬.…\u001d\u0018ØÙøZ×\u0000\u0000\u0000\u0000\u0000",
"header": "00e0ff27f6f596dc1a210647d530ed3b351b5173428370b2086e02000000000000000000217b4aadc25c04da92d8daf863dd2dda507d28b37e664b271629e268bfd491c8934cec676c79021784af443b",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 73,
"name": "ViaBTC",
"slug": "viabtc",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 6253024,
"expectedWeight": 3991868,
"similarity": 0.9862862477811569
}
},
{
"id": "00000000000000000001402065f940b9475159bdc962c92a66d3c6652ffa338a",
"height": 890445,
"version": 601202688,
"timestamp": 1743539574,
"bits": 386038124,
"nonce": 1647397133,
"difficulty": 113757508810854,
"merkle_root": "61d8294afa8f6bafa4d979a77d187dee5f75a6392f957ea647d96eefbbbc5e9b",
"tx_count": 3579,
"size": 1659862,
"weight": 3993406,
"previousblockhash": "00000000000000000001b36ec470ae4ec39f5d975f665d6f40e9d35bfa65290d",
"mediantime": 1743535677,
"stale": false,
"extras": {
"reward": 315617086,
"coinbaseRaw": "034d960d04764dec672f466f756e6472792055534120506f6f6c202364726f70676f6c642f4fac7c451540000000000000",
"orphans": [],
"medianFee": 2.5565329189526835,
"feeRange": [
1.521613832853026,
2,
2.2411347517730498,
3,
3,
3.954954954954955,
162.78343949044586
],
"totalFees": 3117086,
"avgFee": 871,
"avgFeeRate": 3,
"utxoSetChange": 1881,
"avgTxSize": 463.65000000000003,
"totalInputs": 7893,
"totalOutputs": 9774,
"totalOutputAmt": 324878597485,
"segwitTotalTxs": 3189,
"segwitTotalSize": 1538741,
"segwitTotalWeight": 3509030,
"feePercentiles": null,
"virtualSize": 998351.5,
"coinbaseAddress": "bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63",
"coinbaseAddresses": [
"bc1pp7w6kxnj7lzgm29pmuhezwl0vjdlcrthqukll5gn9xuqfq5n673smy4m63",
"bc1qwzrryqr3ja8w7hnja2spmkgfdcgvqwp5swz4af4ngsjecfz0w0pqud7k38"
],
"coinbaseSignature": "OP_PUSHNUM_1 OP_PUSHBYTES_32 0f9dab1a72f7c48da8a1df2f913bef649bfc0d77072dffd11329b8048293d7a3",
"coinbaseSignatureAscii": "\u0003M–\r\u0004vMìg/Foundry USA Pool #dropgold/O¬|E\u0015@\u0000\u0000\u0000\u0000\u0000\u0000",
"header": "00a0d5230d2965fa5bd3e9406f5d665f975d9fc34eae70c46eb3010000000000000000009b5ebcbbef6ed947a67e952f39a6755fee7d187da779d9a4af6b8ffa4a29d861764dec676c7902170d493162",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 111,
"name": "Foundry USA",
"slug": "foundryusa",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 3145370,
"expectedWeight": 3991903,
"similarity": 0.9903353189076812
}
},
{
"id": "00000000000000000001fe3d26b02146d0fc2192db06cbc12478d4b347f5306b",
"height": 890446,
"version": 537722880,
"timestamp": 1743541107,
"bits": 386038124,
"nonce": 826569764,
"difficulty": 113757508810854,
"merkle_root": "d9b320d7cb5aace80ca20b934b13b4a272121fbdd59f3aaba690e0326ca2c144",
"tx_count": 3998,
"size": 1541360,
"weight": 3993545,
"previousblockhash": "00000000000000000001402065f940b9475159bdc962c92a66d3c6652ffa338a",
"mediantime": 1743535803,
"stale": false,
"extras": {
"reward": 317976882,
"coinbaseRaw": "034e960d20202020204d696e656420627920536563706f6f6c2020202070001b04fad5fdfefabe6d6d59dd8ebce6e5aab8fb943bbdcede474b6f2d00a395a717970104a6958c17f1ca100000000000000000008089c9350200",
"orphans": [],
"medianFee": 3.3750830641948864,
"feeRange": [
2.397163120567376,
3,
3,
3.463647199046484,
4.49438202247191,
7.213930348258707,
476.1904761904762
],
"totalFees": 5476882,
"avgFee": 1370,
"avgFeeRate": 5,
"utxoSetChange": 4951,
"avgTxSize": 385.41,
"totalInputs": 7054,
"totalOutputs": 12005,
"totalOutputAmt": 983289729453,
"segwitTotalTxs": 3538,
"segwitTotalSize": 1396505,
"segwitTotalWeight": 3414233,
"feePercentiles": null,
"virtualSize": 998386.25,
"coinbaseAddress": "3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9",
"coinbaseAddresses": [
"3Eif1JfqeMERRsQHtvGEacNN9hhuvnsfe9",
"3Awm3FNpmwrbvAFVThRUFqgpbVuqWisni9"
],
"coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 8ee90177614ecde53314fd67c46162f315852a07 OP_EQUAL",
"coinbaseSignatureAscii": "\u0003N–\r Mined by Secpool p\u0000\u001b\u0004úÕýþú¾mmYݎ¼æåª¸û”;½ÎÞGKo-\u0000£•§\u0017—\u0001\u0004¦•Œ\u0017ñÊ\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000€‰É5\u0002\u0000",
"header": "00000d208a33fa2f65c6d3662ac962c9bd595147b940f96520400100000000000000000044c1a26c32e090a6ab3a9fd5bd1f1272a2b4134b930ba20ce8ac5acbd720b3d97353ec676c79021724744431",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 141,
"name": "SECPOOL",
"slug": "secpool",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 5601814,
"expectedWeight": 3991928,
"similarity": 0.9537877497871488
}
},
{
"id": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab",
"height": 890447,
"version": 568860672,
"timestamp": 1743541240,
"bits": 386038124,
"nonce": 4008077709,
"difficulty": 113757508810854,
"merkle_root": "8c3b098e4e50b67075a4fc52bf4cd603aaa450c240c18a865c9ddc0f27104f5f",
"tx_count": 1919,
"size": 1747789,
"weight": 3993172,
"previousblockhash": "00000000000000000001fe3d26b02146d0fc2192db06cbc12478d4b347f5306b",
"mediantime": 1743536834,
"stale": false,
"extras": {
"reward": 314435106,
"coinbaseRaw": "034f960d0f2f736c7573682f65000002fba05ef1fabe6d6df8d29032ea6f9ab1debd223651f30887df779c6195869e70a7b787b3a15f4b1710000000000000000000ee5f0c00b20200000000",
"orphans": [],
"medianFee": 1.4653828213500366,
"feeRange": [
1.0845070422535212,
1.2,
1.51,
2.0141129032258065,
2.3893805309734515,
4.025477707006369,
300.0065359477124
],
"totalFees": 1935106,
"avgFee": 1008,
"avgFeeRate": 1,
"utxoSetChange": -4244,
"avgTxSize": 910.58,
"totalInputs": 9909,
"totalOutputs": 5665,
"totalOutputAmt": 210763861504,
"segwitTotalTxs": 1720,
"segwitTotalSize": 1629450,
"segwitTotalWeight": 3519924,
"feePercentiles": null,
"virtualSize": 998293,
"coinbaseAddress": "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11",
"coinbaseAddresses": [
"34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11"
],
"coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 1f0cbbec8bc4c945e4e16249b11eee911eded55f OP_EQUAL",
"coinbaseSignatureAscii": "\u0003O–\r\u000f/slush/e\u0000\u0000\u0002û ^ñú¾mmøÒ2êoš±Þ½\"6Qó\b‡ßwœa•†žp§·‡³¡_K\u0017\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000î_\f\u0000²\u0002\u0000\u0000\u0000\u0000",
"header": "0020e8216b30f547b3d47824c1cb06db9221fcd04621b0263dfe010000000000000000005f4f10270fdc9d5c868ac140c250a4aa03d64cbf52fca47570b6504e8e093b8cf853ec676c7902178d69e6ee",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 43,
"name": "Braiins Pool",
"slug": "braiinspool",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 2059571,
"expectedWeight": 3991720,
"similarity": 0.9149852183486826
}
}
],
"mempool-blocks": [
{
"blockSize": 1779311,
"blockVSize": 997968.5,
"nTx": 2132,
"totalFees": 2902870,
"medianFee": 2.0479263387949875,
"feeRange": [
1.0721153846153846,
1.9980563654033041,
2.2195704057279237,
3.009493670886076,
3.4955223880597015,
6.0246913580246915,
218.1818181818182
]
},
{
"blockSize": 1959636,
"blockVSize": 997903.5,
"nTx": 497,
"totalFees": 1093076,
"medianFee": 1.102049424602265,
"feeRange": [
1.0401794819498267,
1.0548148148148149,
1.0548148148148149,
1.0548148148148149,
1.0548148148148149,
1.0761096766260911,
1.1021605957228275
]
},
{
"blockSize": 1477260,
"blockVSize": 997997.25,
"nTx": 720,
"totalFees": 1016195,
"medianFee": 1.007409072434199,
"feeRange": [
1,
1.0019120458891013,
1.0040863981319323,
1.0081019768823594,
1.018450184501845,
1.0203327171903882,
1.0485018498190837
]
},
{
"blockSize": 1021308,
"blockVSize": 431071.5,
"nTx": 823,
"totalFees": 432342,
"medianFee": 0,
"feeRange": [
1,
1,
1,
1.0028011204481793,
1.0042075736325387,
1.0053475935828877,
1.0068649885583525
]
}
]
}

View File

@ -0,0 +1,68 @@
{
"mempool-blocks": [
{
"blockSize": 1780038,
"blockVSize": 997989.75,
"nTx": 2134,
"totalFees": 2919589,
"medianFee": 2.0479263387949875,
"feeRange": [
1.0101010101010102,
2,
2.235576923076923,
3.010452961672474,
3.5240274599542336,
6.032085561497326,
218.1818181818182
]
},
{
"blockSize": 1958446,
"blockVSize": 997996,
"nTx": 503,
"totalFees": 1093277,
"medianFee": 1.102049424602265,
"feeRange": [
1.0101010101010102,
1.0548148148148149,
1.0548148148148149,
1.0548148148148149,
1.067677314564158,
1.0761096766260911,
1.1021605957228275
]
},
{
"blockSize": 1477611,
"blockVSize": 997927.5,
"nTx": 725,
"totalFees": 1016311,
"medianFee": 1.0075971559364956,
"feeRange": [
1,
1.0019334049409236,
1.0042075736325387,
1.0081019768823594,
1.018450184501845,
1.0203327171903882,
1.0548148148148149
]
},
{
"blockSize": 1028219,
"blockVSize": 435137,
"nTx": 833,
"totalFees": 436414,
"medianFee": 0,
"feeRange": [
1,
1,
1,
1.0028011204481793,
1.004231311706629,
1.0053475935828877,
1.0068649885583525
]
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
{
"txReplaced": {
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698"
}
}

View File

@ -0,0 +1,9 @@
{
"ancestors": [],
"bestDescendant": null,
"descendants": [],
"effectiveFeePerVsize": 4.285714285714286,
"sigops": 4,
"fee": 960,
"adjustedVsize": 224
}

View File

@ -0,0 +1,36 @@
{
"replacements": {
"tx": {
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698",
"fee": 960,
"vsize": 224,
"value": 49040,
"rate": 4.285714285714286,
"time": 1743541726,
"rbf": true,
"fullRbf": false
},
"time": 1743541726,
"fullRbf": false,
"replaces": [
{
"tx": {
"txid": "242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29",
"fee": 606,
"vsize": 233,
"value": 49394,
"rate": 2.6008583690987126,
"time": 1743541407,
"rbf": true
},
"time": 1743541407,
"interval": 319,
"fullRbf": false,
"replaces": []
}
]
},
"replaces": [
"242f3fff9ca7d5aea7a7a57d886f3fa7329e24fac948598a991b3a3dd631cd29"
]
}

View File

@ -0,0 +1,38 @@
{
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698",
"version": 2,
"locktime": 0,
"vin": [
{
"txid": "fb16c141d22a3a7af5e44e7478a8663866c35d554cd85107ec8a99b97e5a72e9",
"vout": 0,
"prevout": {
"scriptpubkey": "76a914099f831c49289c7b9cc07a9a632867ecd51a105a88ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 099f831c49289c7b9cc07a9a632867ecd51a105a OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1stAprEZapjCYGUACUcXohQqSHF9MU5Kj",
"value": 50000
},
"scriptsig": "483045022100ab59cf33b33eab1f76286a0fa6962c12171f2133931fec3616f96883551e6c9d02205cacbae788359f2a32ff629e732be0d95843440a3d1c222a63095f83fb69f438014104ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df",
"scriptsig_asm": "OP_PUSHBYTES_72 3045022100ab59cf33b33eab1f76286a0fa6962c12171f2133931fec3616f96883551e6c9d02205cacbae788359f2a32ff629e732be0d95843440a3d1c222a63095f83fb69f43801 OP_PUSHBYTES_65 04ea83ffe426ee6b6827029c72fbdcb0a1602829c1fe384637a7d178aa62a6a1e3d7f29250e001ee708a93ca9771f50ee638aaaaef8941c8a7e2bad5494b23e0df",
"is_coinbase": false,
"sequence": 4294967293
}
],
"vout": [
{
"scriptpubkey": "76a914099f831c49289c7b9cc07a9a632867ecd51a105a88ac",
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 099f831c49289c7b9cc07a9a632867ecd51a105a OP_EQUALVERIFY OP_CHECKSIG",
"scriptpubkey_type": "p2pkh",
"scriptpubkey_address": "1stAprEZapjCYGUACUcXohQqSHF9MU5Kj",
"value": 49040
}
],
"size": 224,
"weight": 896,
"sigops": 4,
"fee": 960,
"status": {
"confirmed": false
}
}

View File

@ -0,0 +1,3 @@
[
1743541726
]

View File

@ -0,0 +1,116 @@
{
"block": {
"id": "000000000000000000019bfe551da67e0d93dda4b355d16f1fdb4031ba7e5d61",
"height": 890448,
"version": 626941952,
"timestamp": 1743541850,
"bits": 386038124,
"nonce": 1177284424,
"difficulty": 113757508810854,
"merkle_root": "563d862ef4ec95ea97735ca5561e347ca6e216cb56bd451e8bedb1014078d6c3",
"tx_count": 2229,
"size": 1763153,
"weight": 3993275,
"previousblockhash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab",
"mediantime": 1743537197,
"stale": false,
"extras": {
"reward": 315498786,
"coinbaseRaw": "0350960d0f2f736c7573682fe500c702ff43d142fabe6d6def42d9effd800b9839c832dcfdc6899e137547f15c8a598dbe3a1363f999a0a310000000000000000000e9a2050064dc00000000",
"orphans": [],
"medianFee": 2.144206217858874,
"feeRange": [
1.0845921450151057,
2,
2.2448979591836733,
3,
3.5985915492957745,
6,
217.0212765957447
],
"totalFees": 2998786,
"avgFee": 1345,
"avgFeeRate": 3,
"utxoSetChange": -3073,
"avgTxSize": 790.84,
"totalInputs": 9558,
"totalOutputs": 6485,
"totalOutputAmt": 442206797883,
"segwitTotalTxs": 1986,
"segwitTotalSize": 1676431,
"segwitTotalWeight": 3646495,
"feePercentiles": null,
"virtualSize": 998318.75,
"coinbaseAddress": "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11",
"coinbaseAddresses": [
"34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11"
],
"coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 1f0cbbec8bc4c945e4e16249b11eee911eded55f OP_EQUAL",
"coinbaseSignatureAscii": "\u0003P–\r\u000f/slush/å\u0000Ç\u0002ÿCÑBú¾mmïBÙïý€\u000b˜9È2ÜýƉž\u0013uGñ\\ŠY¾:\u0013cù™ £\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000é¢\u0005\u0000dÜ\u0000\u0000\u0000\u0000",
"header": "00605e25abe2e310c10e724cfb641859658fee6570ec6062cc0400000000000000000000c3d6784001b1ed8b1e45bd56cb16e2a67c341e56a55c7397ea95ecf42e863d565a56ec676c79021748ef2b46",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 43,
"name": "Braiins Pool",
"slug": "braiinspool",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 3287140,
"expectedWeight": 3991809,
"similarity": 0.9079894021278392
}
},
"mempool-blocks": [
{
"blockSize": 1979907,
"blockVSize": 997974.75,
"nTx": 461,
"totalFees": 1429178,
"medianFee": 1.1020797417745662,
"feeRange": [
1.0666666666666667,
1.0746847720659554,
1.102059530141031,
3,
3.4233409610983982,
5.017605633802817,
148.4084084084084
]
},
{
"blockSize": 1363691,
"blockVSize": 997986.5,
"nTx": 1080,
"totalFees": 1023741,
"medianFee": 1.014827018121911,
"feeRange": [
1,
1.0036011703803736,
1.0054683365672958,
1.0186757215619695,
1.0548148148148149,
1.0548148148148149,
1.068146618482189
]
},
{
"blockSize": 1337253,
"blockVSize": 563516.25,
"nTx": 901,
"totalFees": 564834,
"medianFee": 1.0028011204481793,
"feeRange": [
1,
1,
1,
1.0025062656641603,
1.004231311706629,
1.0053475935828877,
1.0068649885583525
]
}
],
"txConfirmed": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698"
}

View File

@ -0,0 +1,116 @@
{
"block": {
"id": "000000000000000000019bfe551da67e0d93dda4b355d16f1fdb4031ba7e5d61",
"height": 890448,
"version": 626941952,
"timestamp": 1743541850,
"bits": 386038124,
"nonce": 1177284424,
"difficulty": 113757508810854,
"merkle_root": "563d862ef4ec95ea97735ca5561e347ca6e216cb56bd451e8bedb1014078d6c3",
"tx_count": 2229,
"size": 1763153,
"weight": 3993275,
"previousblockhash": "0000000000000000000004cc6260ec7065ee8f65591864fb4c720ec110e3e2ab",
"mediantime": 1743537197,
"stale": false,
"extras": {
"reward": 315498786,
"coinbaseRaw": "0350960d0f2f736c7573682fe500c702ff43d142fabe6d6def42d9effd800b9839c832dcfdc6899e137547f15c8a598dbe3a1363f999a0a310000000000000000000e9a2050064dc00000000",
"orphans": [],
"medianFee": 2.144206217858874,
"feeRange": [
1.0845921450151057,
2,
2.2448979591836733,
3,
3.5985915492957745,
6,
217.0212765957447
],
"totalFees": 2998786,
"avgFee": 1345,
"avgFeeRate": 3,
"utxoSetChange": -3073,
"avgTxSize": 790.84,
"totalInputs": 9558,
"totalOutputs": 6485,
"totalOutputAmt": 442206797883,
"segwitTotalTxs": 1986,
"segwitTotalSize": 1676431,
"segwitTotalWeight": 3646495,
"feePercentiles": null,
"virtualSize": 998318.75,
"coinbaseAddress": "34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11",
"coinbaseAddresses": [
"34XC8GbijKCCvppNvhw4Ra8QZdWsg8tC11"
],
"coinbaseSignature": "OP_HASH160 OP_PUSHBYTES_20 1f0cbbec8bc4c945e4e16249b11eee911eded55f OP_EQUAL",
"coinbaseSignatureAscii": "\u0003P–\r\u000f/slush/å\u0000Ç\u0002ÿCÑBú¾mmïBÙïý€\u000b˜9È2ÜýƉž\u0013uGñ\\ŠY¾:\u0013cù™ £\u0010\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000é¢\u0005\u0000dÜ\u0000\u0000\u0000\u0000",
"header": "00605e25abe2e310c10e724cfb641859658fee6570ec6062cc0400000000000000000000c3d6784001b1ed8b1e45bd56cb16e2a67c341e56a55c7397ea95ecf42e863d565a56ec676c79021748ef2b46",
"utxoSetSize": null,
"totalInputAmt": null,
"pool": {
"id": 43,
"name": "Braiins Pool",
"slug": "braiinspool",
"minerNames": null
},
"matchRate": 100,
"expectedFees": 3287140,
"expectedWeight": 3991809,
"similarity": 0.9079894021278392
}
},
"mempool-blocks": [
{
"blockSize": 1979907,
"blockVSize": 997974.75,
"nTx": 461,
"totalFees": 1429178,
"medianFee": 1.1020797417745662,
"feeRange": [
1.0666666666666667,
1.0746847720659554,
1.102059530141031,
3,
3.4233409610983982,
5.017605633802817,
148.4084084084084
]
},
{
"blockSize": 1363691,
"blockVSize": 997986.5,
"nTx": 1080,
"totalFees": 1023741,
"medianFee": 1.014827018121911,
"feeRange": [
1,
1.0036011703803736,
1.0054683365672958,
1.0186757215619695,
1.0548148148148149,
1.0548148148148149,
1.068146618482189
]
},
{
"blockSize": 1337253,
"blockVSize": 563516.25,
"nTx": 901,
"totalFees": 564834,
"medianFee": 1.0028011204481793,
"feeRange": [
1,
1,
1,
1.0025062656641603,
1.004231311706629,
1.0053475935828877,
1.0068649885583525
]
}
],
"txConfirmed": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698"
}

View File

@ -0,0 +1,75 @@
{
"mempool-blocks": [
{
"blockSize": 1779823,
"blockVSize": 997995.25,
"nTx": 2133,
"totalFees": 2922926,
"medianFee": 2.0479263387949875,
"feeRange": [
1.0825892857142858,
2,
2.2439024390243905,
3.010452961672474,
3.554973821989529,
6.032085561497326,
218.1818181818182
]
},
{
"blockSize": 1957833,
"blockVSize": 997953,
"nTx": 500,
"totalFees": 1093270,
"medianFee": 1.102049424602265,
"feeRange": [
1.0548148148148149,
1.0548148148148149,
1.0548148148148149,
1.0548148148148149,
1.067677314564158,
1.0766488413547237,
1.1021605957228275
]
},
{
"blockSize": 1477864,
"blockVSize": 997999,
"nTx": 730,
"totalFees": 1016458,
"medianFee": 1.0075971559364956,
"feeRange": [
1,
1.0019552465783186,
1.004255319148936,
1.0081019768823594,
1.018450184501845,
1.0203327171903882,
1.0548148148148149
]
},
{
"blockSize": 1030954,
"blockVSize": 436613.5,
"nTx": 838,
"totalFees": 437891,
"medianFee": 0,
"feeRange": [
1,
1,
1,
1.0026525198938991,
1.004231311706629,
1.0053475935828877,
1.0068649885583525
]
}
],
"txPosition": {
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698",
"position": {
"block": 0,
"vsize": 111102
}
}
}

View File

@ -0,0 +1,75 @@
{
"mempool-blocks": [
{
"blockSize": 1719945,
"blockVSize": 997952.25,
"nTx": 2558,
"totalFees": 3287140,
"medianFee": 2.4046448299072485,
"feeRange": [
1.073446327683616,
2,
2.2567567567567566,
3.0106761565836297,
3.6169014084507043,
6.015037593984962,
218.1818181818182
]
},
{
"blockSize": 2022898,
"blockVSize": 997983.25,
"nTx": 131,
"totalFees": 1098129,
"medianFee": 1.1020797417745662,
"feeRange": [
1.0625,
1.0691217722793642,
1.073436083408885,
1.0761096766260911,
1.080091533180778,
1.102110739151618,
1.1021909190121146
]
},
{
"blockSize": 1363844,
"blockVSize": 997998.5,
"nTx": 1073,
"totalFees": 1023651,
"medianFee": 1.014827018121911,
"feeRange": [
1,
1.003584229390681,
1.0054683365672958,
1.0186757215619695,
1.0548148148148149,
1.0548148148148149,
1.068146618482189
]
},
{
"blockSize": 1335390,
"blockVSize": 562453.5,
"nTx": 902,
"totalFees": 563772,
"medianFee": 1.0028011204481793,
"feeRange": [
1,
1,
1,
1.0025402201524132,
1.004231311706629,
1.0053475935828877,
1.0068649885583525
]
}
],
"txPosition": {
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698",
"position": {
"block": 0,
"vsize": 128920
}
}
}

View File

@ -0,0 +1,9 @@
{
"txPosition": {
"txid": "b6a53d8a8025c0c5b194345925a99741b645cae0b1f180f0613273029f2bf698",
"position": {
"block": 0,
"vsize": 110880
}
}
}

View File

@ -0,0 +1,37 @@
{
"rbfLatest": [
{
"tx": {
"txid": "f4bae4f626036250fd00d68490e572f65f66417452003a0f4c4d76f17a9fde68",
"fee": 1185,
"vsize": 223,
"value": 41729,
"rate": 5.313901345291479,
"time": 1743587177,
"rbf": true,
"fullRbf": false,
"mined": true
},
"time": 1743587177,
"fullRbf": true,
"replaces": [
{
"tx": {
"txid": "12945412dfc455e0ed6049dc2ee8737756c8d9e2d9a2eb26f366cd5019a0369f",
"fee": 504,
"vsize": 222,
"value": 42410,
"rate": 2.27027027027027,
"time": 1743586081,
"rbf": true
},
"time": 1743586081,
"interval": 1096,
"fullRbf": false,
"replaces": []
}
],
"mined": true
}
]
}

View File

@ -0,0 +1,68 @@
{
"rbfLatest": [
{
"tx": {
"txid": "d313b479acfbae719afb488a078e0fe0e052a67b9f65f73f7c75d3d95fd36acc",
"fee": 672,
"vsize": 167.25,
"value": 29996328,
"rate": 4.017937219730942,
"time": 1743587365,
"rbf": true,
"fullRbf": false
},
"time": 1743587365,
"fullRbf": false,
"replaces": [
{
"tx": {
"txid": "eb5aa786cabda307cc9642cfb9c41a3b405ac20a391eefbe54be7930bea61865",
"fee": 336,
"vsize": 167.5,
"value": 29996664,
"rate": 2.005970149253731,
"time": 1743586424,
"rbf": true
},
"time": 1743586424,
"interval": 941,
"fullRbf": false,
"replaces": []
}
]
},
{
"tx": {
"txid": "f4bae4f626036250fd00d68490e572f65f66417452003a0f4c4d76f17a9fde68",
"fee": 1185,
"vsize": 223,
"value": 41729,
"rate": 5.313901345291479,
"time": 1743587177,
"rbf": true,
"fullRbf": false,
"mined": true
},
"time": 1743587177,
"fullRbf": true,
"replaces": [
{
"tx": {
"txid": "12945412dfc455e0ed6049dc2ee8737756c8d9e2d9a2eb26f366cd5019a0369f",
"fee": 504,
"vsize": 222,
"value": 42410,
"rate": 2.27027027027027,
"time": 1743586081,
"rbf": true
},
"time": 1743586081,
"interval": 1096,
"fullRbf": false,
"replaces": []
}
],
"mined": true
}
]
}

View File

@ -44,6 +44,7 @@
import { PageIdleDetector } from './PageIdleDetector';
import { mockWebSocket } from './websocket';
import { mockWebSocketV2 } from './websocket';
/* global Cypress */
const codes = {
@ -72,6 +73,10 @@ Cypress.Commands.add('mockMempoolSocket', () => {
mockWebSocket();
});
Cypress.Commands.add('mockMempoolSocketV2', () => {
mockWebSocketV2();
});
Cypress.Commands.add('changeNetwork', (network: "testnet" | "testnet4" | "signet" | "liquid" | "mainnet") => {
cy.get('.dropdown-toggle').click().then(() => {
cy.get(`a.${network}`).click().then(() => {

View File

@ -5,6 +5,7 @@ declare namespace Cypress {
waitForSkeletonGone(): Chainable<any>
waitForPageIdle(): Chainable<any>
mockMempoolSocket(): Chainable<any>
mockMempoolSocketV2(): Chainable<any>
changeNetwork(network: "testnet"|"testnet4"|"signet"|"liquid"|"mainnet"): Chainable<any>
}
}

View File

@ -27,6 +27,37 @@ const createMock = (url: string) => {
return mocks[url];
};
export const mockWebSocketV2 = () => {
cy.on('window:before:load', (win) => {
const winWebSocket = win.WebSocket;
cy.stub(win, 'WebSocket').callsFake((url) => {
console.log(url);
if ((new URL(url).pathname.indexOf('/sockjs-node/') !== 0)) {
const { server, websocket } = createMock(url);
win.mockServer = server;
win.mockServer.on('connection', (socket) => {
win.mockSocket = socket;
});
win.mockServer.on('message', (message) => {
console.log(message);
});
return websocket;
} else {
return new winWebSocket(url);
}
});
});
cy.on('window:before:unload', () => {
for (const url in mocks) {
cleanupMock(url);
}
});
};
export const mockWebSocket = () => {
cy.on('window:before:load', (win) => {
const winWebSocket = win.WebSocket;
@ -65,6 +96,27 @@ export const mockWebSocket = () => {
});
};
export const receiveWebSocketMessageFromServer = ({
params
}: { params?: any } = {}) => {
cy.window().then((win) => {
if (params.message) {
console.log('sending message');
win.mockSocket.send(params.message.contents);
}
if (params.file) {
cy.readFile(`cypress/fixtures/${params.file.path}`, 'utf-8').then((fixture) => {
console.log('sending payload');
win.mockSocket.send(JSON.stringify(fixture));
});
}
});
return;
};
export const emitMempoolInfo = ({
params
}: { params?: any } = {}) => {
@ -82,16 +134,22 @@ export const emitMempoolInfo = ({
switch (params.command) {
case "init": {
win.mockSocket.send('{"conversions":{"USD":32365.338815782445}}');
cy.readFile('cypress/fixtures/mainnet_live2hchart.json', 'ascii').then((fixture) => {
cy.readFile('cypress/fixtures/mainnet_live2hchart.json', 'utf-8').then((fixture) => {
win.mockSocket.send(JSON.stringify(fixture));
});
cy.readFile('cypress/fixtures/mainnet_mempoolInfo.json', 'ascii').then((fixture) => {
cy.readFile('cypress/fixtures/mainnet_mempoolInfo.json', 'utf-8').then((fixture) => {
win.mockSocket.send(JSON.stringify(fixture));
});
break;
}
case "rbfTransaction": {
cy.readFile('cypress/fixtures/mainnet_rbf.json', 'ascii').then((fixture) => {
cy.readFile('cypress/fixtures/mainnet_rbf.json', 'utf-8').then((fixture) => {
win.mockSocket.send(JSON.stringify(fixture));
});
break;
}
case 'trackTx': {
cy.readFile('cypress/fixtures/track_tx.json', 'utf-8').then((fixture) => {
win.mockSocket.send(JSON.stringify(fixture));
});
break;

View File

@ -25,14 +25,12 @@
"i18n-extract-from-source": "npm run ng -- extract-i18n --out-file ./src/locale/messages.xlf",
"i18n-pull-from-transifex": "tx pull -a --parallel --minimum-perc 1 --force",
"serve": "npm run generate-config && npm run ng -- serve -c local",
"serve:stg": "npm run generate-config && npm run ng -- serve -c staging",
"serve:local-prod": "npm run generate-config && npm run ng -- serve -c local-prod",
"serve:local-staging": "npm run generate-config && npm run ng -- serve -c local-staging",
"serve:parameterized": "npm run generate-config && npm run ng -- serve -c parameterized",
"start": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local",
"start:parameterized": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c parameterized",
"start:local-esplora": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-esplora",
"start:stg": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c staging",
"start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod",
"start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
"start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed",
"build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets-dev && npm run sync-assets && npm run build-mempool.js",
"sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'",
@ -58,8 +56,8 @@
"cypress:run:record": "cypress run --record",
"cypress:open:ci": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open",
"cypress:run:ci": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record",
"cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open",
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
"cypress:open:ci:parameterized": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:parameterized 4200 cypress:open",
"cypress:run:ci:parameterized": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:parameterized 4200 cypress:run:record"
},
"dependencies": {
"@angular-devkit/build-angular": "^17.3.1",
@ -123,4 +121,4 @@
"scarfSettings": {
"enabled": false
}
}
}

View File

@ -0,0 +1,36 @@
const fs = require('fs');
const PROXY_CONFIG = require('./proxy.conf');
const addApiKeyHeader = (proxyReq, req, res) => {
if (process.env.MEMPOOL_CI_API_KEY) {
proxyReq.setHeader('X-Mempool-Auth', process.env.MEMPOOL_CI_API_KEY);
}
};
PROXY_CONFIG.forEach((entry) => {
const mempoolHostname = process.env.MEMPOOL_HOSTNAME
? process.env.MEMPOOL_HOSTNAME
: 'mempool.space';
const liquidHostname = process.env.LIQUID_HOSTNAME
? process.env.LIQUID_HOSTNAME
: 'liquid.network';
entry.target = entry.target.replace('mempool.space', mempoolHostname);
entry.target = entry.target.replace('liquid.network', liquidHostname);
if (entry.onProxyReq) {
const originalProxyReq = entry.onProxyReq;
entry.onProxyReq = (proxyReq, req, res) => {
originalProxyReq(proxyReq, req, res);
if (process.env.MEMPOOL_CI_API_KEY) {
proxyReq.setHeader('X-Mempool-Auth', process.env.MEMPOOL_CI_API_KEY);
}
};
} else {
entry.onProxyReq = addApiKeyHeader;
}
});
module.exports = PROXY_CONFIG;

View File

@ -1,12 +0,0 @@
const fs = require('fs');
let PROXY_CONFIG = require('./proxy.conf');
PROXY_CONFIG.forEach(entry => {
const hostname = process.env.CYPRESS_REROUTE_TESTNET === 'true' ? 'mempool-staging.fra.mempool.space' : 'node201.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.va1.mempool.space");
});
module.exports = PROXY_CONFIG;

View File

@ -147,6 +147,15 @@
</svg>
<span>Bull Bitcoin</span>
</a>
<a href="https://fortris.com/" target="_blank" title="Fortris">
<svg id="fortris-logo" viewBox="0 0 140.08 129.13" version="1.1" width="74px" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
<defs><style>.fortris-cls-1{fill:#29384a;}.fortris-cls-1,.fortris-cls-2{stroke-width:0px;}.fortris-cls-2{fill:#ff6e72;}</style></defs>
<rect class="fortris-cls-2" x="0" y="0" width="140.08" height="30.59" rx="15.29" ry="15.29" />
<rect class="fortris-cls-2" x="0" y="98.540001" width="69.779999" height="30.59" rx="15.29" ry="15.29" />
<rect class="fortris-cls-2" x="0" y="49.27" width="109.5" height="30.59" rx="15.29" ry="15.29" />
</svg>
<span>Fortris</span>
</a>
<a href="https://exodus.com/" target="_blank" title="Exodus">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
@ -451,7 +460,7 @@
Trademark Notice<br>
</div>
<p>
The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
The Mempool Open Source Project&reg;, Mempool Accelerator&reg;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
</p>
<p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.

View File

@ -264,6 +264,6 @@
display: flex;
flex-wrap: wrap;
justify-content: center;
max-width: 800px;
max-width: 850px;
}
}
}

View File

@ -158,7 +158,7 @@
<!-- MEMPOOL BASE FEE -->
<tr>
<td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator fees</td>
<td class="item" i18n="accelerator.mempool-accelerator-fees">Mempool Accelerator® fees</td>
</tr>
<tr class="info" [class.group-last]="!estimate.vsizeFee" [class.dashed-bottom]="!estimate.vsizeFee">
<td class="info">
@ -567,14 +567,29 @@
} @else if (step === 'success') {
<div class="row mb-1 text-center">
<div class="col-sm">
<h1 style="font-size: larger;"><ng-content select="[slot='accelerated-title']"></ng-content><span class="default-slot" i18n="accelerator.success-message">Your transaction is being accelerated!</span></h1>
<h1 style="font-size: larger;"><ng-content select="[slot='accelerated-title']"></ng-content>
@if (accelerationResponse) {
<span class="default-slot" i18n="accelerator.success-message">Your transaction is being accelerated!</span>
} @else {
<span class="default-slot" i18n="accelerator.success-message-third-party">Transaction is already being accelerated!</span>
}
</h1>
</div>
</div>
<div class="row text-center mt-1">
<div class="col-sm">
<div class="d-flex flex-row justify-content-center align-items-center">
<span i18n="accelerator.confirmed-acceleration-with-miners">Your transaction has been accepted for acceleration by our mining pool partners.</span>
@if (accelerationResponse) {
<span i18n="accelerator.confirmed-acceleration-with-miners">Your transaction has been accepted for acceleration by our mining pool partners.</span>
} @else {
<span i18n="accelerator.confirmed-acceleration-with-miners-third-party">Transaction has already been accepted for acceleration by our mining pool partners.</span>
}
</div>
@if (accelerationResponse?.receiptUrl) {
<div class="d-flex flex-row justify-content-center align-items-center">
<span i18n="accelerator.receipt-label"><a [href]="accelerationResponse.receiptUrl" target="_blank">Click here to get a receipt.</a></span>
</div>
}
</div>
</div>
<hr>

View File

@ -87,6 +87,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
math = Math;
isMobile: boolean = window.innerWidth <= 767.98;
isProdDomain = false;
accelerationResponse: { receiptUrl: string | null } | undefined;
private _step: CheckoutStep = 'summary';
simpleMode: boolean = true;
@ -194,16 +195,12 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.scrollToElement('acceleratePreviewAnchor', 'start');
}
if (changes.accelerating && this.accelerating) {
if (this.step === 'processing' || this.step === 'paid') {
this.moveToStep('success', true);
} else { // Edge case where the transaction gets accelerated by someone else or on another session
this.closeModal();
}
this.moveToStep('success', true);
}
}
moveToStep(step: CheckoutStep, force: boolean = false): void {
if (this.isCheckoutLocked > 0 && !force) {
if (this.isCheckoutLocked > 0 && !force || this.step === 'success') {
return;
}
this.processing = false;
@ -525,7 +522,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
console.error(`Cannot retrieve payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return;
@ -541,7 +538,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
costUSD
).subscribe({
next: () => {
next: (response) => {
this.accelerationResponse = response;
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
@ -643,7 +641,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
if (tokenResult?.status === 'OK') {
const card = tokenResult.details?.card;
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
console.error(`Cannot retreive payment card details`);
console.error(`Cannot retrieve payment card details`);
this.accelerateError = 'apple_pay_no_card_details';
this.processing = false;
return;
@ -668,7 +666,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
costUSD,
verificationToken.userChallenged
).subscribe({
next: () => {
next: (response) => {
this.accelerationResponse = response;
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
@ -777,7 +776,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
costUSD,
verificationToken.userChallenged
).subscribe({
next: () => {
next: (response) => {
this.accelerationResponse = response;
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
@ -870,7 +870,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.details.cashAppPay.referenceId,
costUSD
).subscribe({
next: () => {
next: (response) => {
this.accelerationResponse = response;
this.processing = false;
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
this.audioService.playSound('ascend-chime-cartoon');
@ -936,7 +937,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.loadingBtcpayInvoice = true;
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
switchMap(response => {
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
return this.servicesApiService.retrieveInvoice$(response.btcpayInvoiceId);
}),
catchError(error => {
console.log(error);

View File

@ -44,6 +44,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
@Input() right: number | string = 10;
@Input() left: number | string = 70;
@Input() widget: boolean = false;
@Input() label: string = '';
@Input() defaultFiat: boolean = false;
@Input() showLegend: boolean = true;
@Input() showYAxis: boolean = true;
@ -55,6 +56,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
hoverData: any[] = [];
conversions: any;
allowZoom: boolean = false;
labelGraphic: any;
selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false };
@ -85,6 +87,18 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true;
this.labelGraphic = this.label ? {
type: 'text',
right: '36px',
bottom: '36px',
z: 100,
silent: true,
style: {
fill: '#fff',
text: this.label,
font: '24px sans-serif'
}
} : undefined;
if (!this.addressSummary$ && (!this.address || !this.stats)) {
return;
}
@ -205,6 +219,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
right: this.adjustedRight,
left: this.adjustedLeft,
},
graphic: this.labelGraphic ? [{
...this.labelGraphic,
right: this.adjustedRight + 22 + 'px',
}] : undefined,
legend: (this.showLegend && !this.stateService.isAnyTestnet()) ? {
data: [
{
@ -443,6 +461,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
right: this.adjustedRight,
left: this.adjustedLeft,
},
graphic: this.labelGraphic ? [{
...this.labelGraphic,
right: this.adjustedRight + 22 + 'px',
}] : undefined,
legend: {
selected: this.selected,
},

View File

@ -238,7 +238,7 @@
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
<app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address ? address.chain_stats : null" [widget]="true" [defaultFiat]="true" [height]="graphHeight"></app-address-graph>
<app-address-graph [address]="widget.props.address" [addressSummary$]="addressSummary$" [period]="widget.props.period || 'all'" [stats]="address ? address.chain_stats : null" [widget]="true" [defaultFiat]="true" [height]="graphHeight" [label]="widget.props.label"></app-address-graph>
</div>
</div>
</div>
@ -272,7 +272,7 @@
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
<app-address-graph [addressSummary$]="walletSummary$" [period]="widget.props.period || 'all'" [widget]="true" [height]="graphHeight"></app-address-graph>
<app-address-graph [addressSummary$]="walletSummary$" [period]="widget.props.period || 'all'" [widget]="true" [height]="graphHeight" [label]="widget.props.label"></app-address-graph>
</div>
</div>
</div>

View File

@ -70,11 +70,6 @@
<li class="nav-item" routerLinkActive="active" id="btn-graphs">
<a class="nav-link" [routerLink]="['/graphs' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'chart-area']" [fixedWidth]="true" i18n-title="master-page.graphs" title="Graphs"></fa-icon></a>
</li>
<!--
<li class="nav-item d-none d-lg-block" routerLinkActive="active">
<a class="nav-link" [routerLink]="['/tv' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon></a>
</li>
-->
<li class="nav-item" routerLinkActive="active" id="btn-assets">
<a class="nav-link" [routerLink]="['/assets' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'database']" [fixedWidth]="true" i18n-title="master-page.assets" title="Assets"></fa-icon></a>
</li>

View File

@ -45,14 +45,14 @@
<br>
<h4>USING MEMPOOL ACCELERATOR&trade;</h4>
<h4>USING MEMPOOL ACCELERATOR&reg;</h4>
<p *ngIf="officialMempoolSpace">If you use Mempool Accelerator&trade; your acceleration request will be sent to us and relayed to Mempool's mining pool partners. We will store the TXID of the transactions you accelerate with us. We share this information with our mining pool partners, and publicly display accelerated transaction details on our website and APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.</p>
<p *ngIf="officialMempoolSpace">If you use Mempool Accelerator&reg; your acceleration request will be sent to us and relayed to Mempool's mining pool partners. We will store the TXID of the transactions you accelerate with us. We share this information with our mining pool partners, and publicly display accelerated transaction details on our website and APIs. No personal information or account identifiers will be shared with any third party including mining pool partners.</p>
<p *ngIf="!officialMempoolSpace">When using Mempool Accelerator&trade; the mempool.space privacy policy will apply: <a href="https://mempool.space/privacy-policy">https://mempool.space/privacy-policy</a>.</p>
<p *ngIf="!officialMempoolSpace">When using Mempool Accelerator&reg; the mempool.space privacy policy will apply: <a href="https://mempool.space/privacy-policy">https://mempool.space/privacy-policy</a>.</p>
<br>
<ng-container *ngIf="officialMempoolSpace">
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
@ -67,7 +67,7 @@
<li>If you sign up for a subscription to Mempool Enterprise&trade; we also collect your company name which is not shared with any third-party.</li>
<li>If you sign up for an account on mempool.space and use Mempool Accelerator&trade; Pro your accelerated transactions will be associated with your account for the purposes of accounting.</li>
<li>If you sign up for an account on mempool.space and use Mempool Accelerator&reg; Pro your accelerated transactions will be associated with your account for the purposes of accounting.</li>
</ul>
@ -101,7 +101,7 @@
<p>We aim to retain your data only as long as necessary:</p>
<ul>
<li>An account is considered inactive if all of the following conditions are met: a) No login activity within the past 6 months, b) No active subscriptions associated with the account, c) No Mempool Accelerator Pro account credit</li>
<li>An account is considered inactive if all of the following conditions are met: a) No login activity within the past 6 months, b) No active subscriptions associated with the account, c) No Mempool Accelerator® Pro account credit</li>
<li>If an account meets the criteria for inactivity as defined above, we will automatically delete the associated account data after a period of 6 months of continuous inactivity, except in the case of payment disputes or account irregularities.</li>
</ul>

View File

@ -16,9 +16,6 @@
<a class="btn btn-primary btn-sm mb-0" [routerLink]="['/clock/mempool/0' | relativeUrl]" style="color: white" id="btn-clock">
<fa-icon [icon]="['fas', 'clock']" [fixedWidth]="true" i18n-title="master-page.clockview" i18n-title="footer.clock-mempool" title="Clock (Mempool)"></fa-icon>
</a>
<a *ngIf="!isMobile()" class="btn btn-primary btn-sm mb-0" [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv">
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
</a>
</div>
<div class="btn-toggle-rows" name="radioBasic">
<div class="btn-group btn-group-toggle">

File diff suppressed because one or more lines are too long

View File

@ -1,25 +0,0 @@
<div id="tv-wrapper">
<div class="tv-container">
<div class="chart-holder">
<app-mempool-graph
[template]="'advanced'"
[height]="600"
[left]="60"
[right]="10"
[data]="statsSubscription$ | async"
[showZoom]="false"
></app-mempool-graph>
</div>
<div class="blockchain-wrapper" [dir]="timeLtr ? 'rtl' : 'ltr'" [class.time-ltr]="timeLtr">
<div class="position-container">
<span>
<div class="blocks-wrapper">
<app-mempool-blocks></app-mempool-blocks>
<app-blockchain-blocks></app-blockchain-blocks>
</div>
<div id="divider"></div>
</span>
</div>
</div>
</div>
</div>

View File

@ -1,80 +0,0 @@
.loading {
margin: auto;
width: 100%;
display: flex;
text-align: center;
justify-content: center;
height: 100vh;
align-items: center;
}
#tv-wrapper {
height: 100vh;
overflow: hidden;
position: relative;
}
.chart-holder {
position: relative;
height: 655px;
width: 100%;
margin: 30px auto 0;
}
.blockchain-wrapper {
display: block;
height: 100%;
min-height: 240px;
position: relative;
top: 30px;
.position-container {
position: absolute;
left: 0;
bottom: 170px;
transform: translateX(50vw);
}
#divider {
width: 2px;
height: 175px;
left: 0;
top: -40px;
position: absolute;
img {
position: absolute;
left: -100px;
top: -28px;
}
}
&.time-ltr {
.blocks-wrapper {
transform: scaleX(-1);
}
}
}
:host-context(.ltr-layout) {
.blockchain-wrapper.time-ltr .blocks-wrapper,
.blockchain-wrapper .blocks-wrapper {
direction: ltr;
}
}
:host-context(.rtl-layout) {
.blockchain-wrapper.time-ltr .blocks-wrapper,
.blockchain-wrapper .blocks-wrapper {
direction: rtl;
}
}
.tv-container {
display: flex;
margin-top: 0px;
flex-direction: column;
}

View File

@ -1,86 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { WebsocketService } from '@app/services/websocket.service';
import { OptimizedMempoolStats } from '@interfaces/node-api.interface';
import { StateService } from '@app/services/state.service';
import { ApiService } from '@app/services/api.service';
import { SeoService } from '@app/services/seo.service';
import { ActivatedRoute } from '@angular/router';
import { map, scan, startWith, switchMap, tap } from 'rxjs/operators';
import { interval, merge, Observable, Subscription } from 'rxjs';
import { ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-television',
templateUrl: './television.component.html',
styleUrls: ['./television.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TelevisionComponent implements OnInit, OnDestroy {
mempoolStats: OptimizedMempoolStats[] = [];
statsSubscription$: Observable<OptimizedMempoolStats[]>;
fragment: string;
timeLtrSubscription: Subscription;
timeLtr: boolean = this.stateService.timeLtr.value;
constructor(
private websocketService: WebsocketService,
private apiService: ApiService,
private stateService: StateService,
private seoService: SeoService,
private route: ActivatedRoute
) { }
refreshStats(time: number, fn: Observable<OptimizedMempoolStats[]>) {
return interval(time).pipe(startWith(0), switchMap(() => fn));
}
ngOnInit() {
this.seoService.setTitle($localize`:@@46ce8155c9ab953edeec97e8950b5a21e67d7c4e:TV view`);
this.seoService.setDescription($localize`:@@meta.description.tv:See Bitcoin blocks and mempool congestion in real-time in a simplified format perfect for a TV.`);
this.websocketService.want(['blocks', 'live-2h-chart', 'mempool-blocks']);
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
this.timeLtr = !!ltr;
});
this.statsSubscription$ = merge(
this.stateService.live2Chart$.pipe(map(stats => [stats])),
this.route.fragment
.pipe(
tap(fragment => { this.fragment = fragment ?? '2h'; }),
switchMap((fragment) => {
const minute = 60000; const hour = 3600000;
switch (fragment) {
case '24h': return this.apiService.list24HStatistics$();
case '1w': return this.refreshStats(5 * minute, this.apiService.list1WStatistics$());
case '1m': return this.refreshStats(30 * minute, this.apiService.list1MStatistics$());
case '3m': return this.refreshStats(2 * hour, this.apiService.list3MStatistics$());
case '6m': return this.refreshStats(3 * hour, this.apiService.list6MStatistics$());
case '1y': return this.refreshStats(8 * hour, this.apiService.list1YStatistics$());
case '2y': return this.refreshStats(8 * hour, this.apiService.list2YStatistics$());
case '3y': return this.refreshStats(12 * hour, this.apiService.list3YStatistics$());
default /* 2h */: return this.apiService.list2HStatistics$();
}
})
)
)
.pipe(
scan((mempoolStats, newStats) => {
if (newStats.length > 1) {
mempoolStats = newStats;
} else if (['2h', '24h'].includes(this.fragment)) {
mempoolStats.unshift(newStats[0]);
const now = Date.now() / 1000;
const start = now - (this.fragment === '2h' ? (2 * 60 * 60) : (24 * 60 * 60) );
mempoolStats = mempoolStats.filter(p => p.added >= start);
}
return mempoolStats;
})
);
}
ngOnDestroy() {
this.timeLtrSubscription.unsubscribe();
}
}

View File

@ -67,9 +67,9 @@
</ng-container>
<h4>MEMPOOL ACCELERATOR&trade;</h4>
<h4>MEMPOOL ACCELERATOR&reg;</h4>
<p><a href="https://mempool.space/accelerator">Mempool Accelerator&trade;</a> enables members of the Bitcoin community to submit requests for transaction prioritization. </p>
<p><a href="https://mempool.space/accelerator">Mempool Accelerator&reg;</a> enables members of the Bitcoin community to submit requests for transaction prioritization. </p>
<ul>
<li>Mempool will use reasonable commercial efforts to relay user acceleration requests to Mempool's mining pool partners, but it is at the discretion of Mempool and Mempool's mining pool partners to accept acceleration requests. </li>
@ -84,11 +84,11 @@
<br>
<li>All acceleration payments and Mempool Accelerator&trade; account credit top-ups are non-refundable. </li>
<li>All acceleration payments and Mempool Accelerator&reg; account credit top-ups are non-refundable. </li>
<br>
<li>Mempool Accelerator&trade; account credit top-ups are prepayment for future accelerations and cannot be withdrawn or transferred.</li>
<li>Mempool Accelerator&reg; account credit top-ups are prepayment for future accelerations and cannot be withdrawn or transferred.</li>
<br>

View File

@ -8,7 +8,7 @@
<div *ngIf="officialMempoolSpace">
<h2>Trademark Policy and Guidelines</h2>
<h5>The Mempool Open Source Project &reg;</h5>
<h6>Updated: August 19, 2024</h6>
<h6>Updated: February 11, 2025</h6>
<br>
<div class="text-left">
@ -59,6 +59,7 @@
<tr><td>Mempool Accelerator</td></tr>
<tr><td>Mempool Enterprise</td></tr>
<tr><td>Mempool Liquidity</td></tr>
<tr><td>Mempool</td></tr>
<tr><td>mempool.space</td></tr>
<tr><td>Be your own explorer</td></tr>
<tr><td>Explore the full Bitcoin ecosystem</td></tr>
@ -340,7 +341,8 @@
<p>Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:</p>
<p>"The Mempool Open Source Project&reg;, Mempool Accelerator&trade;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, the Mempool Accelerator logo;, the Mempool Goggles logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."</p>
<p>"The Mempool Open Source Project&reg;, Mempool Accelerator&reg;, Mempool Enterprise&reg;, Mempool Liquidity&trade;, Mempool&reg;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, the Mempool Accelerator logo;, the Mempool Goggles logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."</p>
<li>What to Do When You See Abuse</li>
<br>

View File

@ -170,7 +170,7 @@
@if (!tx?.acceleration && acceleratorAvailable && accelerateCtaType === 'button' && !showAccelerationSummary && notAcceleratedOnLoad) {
<div class="d-flex accelerate">
<a class="btn btn-sm accelerateDeepMempool btn-small-height" [class.disabled]="!eligibleForAcceleration" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
<a *ngIf="!eligibleForAcceleration" href="https://mempool.space/accelerator#why-cant-accelerate" target="_blank" class="info-badges ml-1" i18n-ngbTooltip="Mempool Accelerator&trade; tooltip" ngbTooltip="This transaction cannot be accelerated">
<a *ngIf="!eligibleForAcceleration" href="https://mempool.space/accelerator#why-cant-accelerate" target="_blank" class="info-badges ml-1" i18n-ngbTooltip="Mempool Accelerator&reg; tooltip" ngbTooltip="This transaction cannot be accelerated">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
</a>
</div>
@ -318,4 +318,4 @@
<tr>
<td><span class="skeleton-loader"></span></td>
</tr>
</ng-template>
</ng-template>

View File

@ -30,7 +30,6 @@
</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()">&#10005;</button>
</div>
@ -40,14 +39,18 @@
<div class="clearfix"></div>
<div *ngIf="!successBroadcast" class="alert alert-mempool" style="align-items: center;">
<div 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.">
<ng-container *ngIf="!successBroadcast" i18n="transaction.local-tx|This transaction is stored locally in your browser.">
This transaction is stored locally in your browser. Broadcast it to add it to the mempool.
</ng-container>
<ng-container *ngIf="successBroadcast" i18n="transaction.redirecting|Redirecting to transaction page...">
Redirecting to transaction page...
</ng-container>
</span>
<button [disabled]="isLoadingBroadcast" type="button" class="btn btn-sm btn-primary" i18n="transaction.broadcast|Broadcast" (click)="postTx()">Broadcast</button>
<button *ngIf="!successBroadcast" [disabled]="isLoadingBroadcast" type="button" class="btn btn-sm btn-primary btn-broadcast" i18n="transaction.broadcast|Broadcast" (click)="postTx()">Broadcast</button>
<button *ngIf="successBroadcast" type="button" class="btn btn-sm btn-success no-cursor btn-broadcast" i18n="transaction.broadcasted|Broadcasted">Broadcasted</button>
</div>
@if (!hasPrevouts) {

View File

@ -191,4 +191,12 @@
.no-cursor {
cursor: default !important;
pointer-events: none;
}
.btn-broadcast {
margin-left: 5px;
@media (max-width: 567px) {
margin-left: 0;
margin-top: 5px;
}
}

View File

@ -3,7 +3,7 @@ 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 { catchError, firstValueFrom, Subscription, switchMap, tap, throwError, timer } from 'rxjs';
import { WebsocketService } from '../../services/websocket.service';
import { ActivatedRoute, Router } from '@angular/router';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
@ -36,6 +36,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
isLoadingBroadcast: boolean;
errorBroadcast: string;
successBroadcast: boolean;
broadcastSubscription: Subscription;
isMobile: boolean;
@ViewChild('graphContainer')
@ -82,7 +83,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
this.resetState();
this.isLoading = true;
try {
const { tx, hex } = decodeRawTransaction(this.pushTxForm.get('txRaw').value, this.stateService.network);
const { tx, hex } = decodeRawTransaction(this.pushTxForm.get('txRaw').value.trim(), this.stateService.network);
await this.fetchPrevouts(tx);
await this.fetchCpfpInfo(tx);
this.processTransaction(tx, hex);
@ -207,18 +208,22 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
});
}
async postTx(): Promise<string> {
postTx(): void {
this.isLoadingBroadcast = true;
this.errorBroadcast = null;
return new Promise((resolve, reject) => {
this.apiService.postTransaction$(this.rawHexTransaction)
.subscribe((result) => {
this.broadcastSubscription = this.apiService.postTransaction$(this.rawHexTransaction).pipe(
tap((txid: string) => {
this.isLoadingBroadcast = false;
this.successBroadcast = true;
this.transaction.txid = result;
resolve(result);
},
(error) => {
this.transaction.txid = txid;
}),
switchMap((txid: string) =>
timer(2000).pipe(
tap(() => this.router.navigate([this.relativeUrlPipe.transform('/tx/' + txid)])),
)
),
catchError((error) => {
if (typeof error.error === 'string') {
const matchText = error.error.replace(/\\/g, '').match('"message":"(.*?)"');
this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + (matchText && matchText[1] || error.error);
@ -226,9 +231,9 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
this.errorBroadcast = 'Failed to broadcast transaction, reason: ' + error.message;
}
this.isLoadingBroadcast = false;
reject(this.error);
});
});
return throwError(() => error);
})
).subscribe();
}
resetState() {
@ -253,6 +258,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
this.missingPrevouts = [];
this.stateService.markBlock$.next({});
this.mempoolBlocksSubscription?.unsubscribe();
this.broadcastSubscription?.unsubscribe();
}
resetForm() {
@ -308,6 +314,7 @@ export class TransactionRawComponent implements OnInit, OnDestroy {
this.mempoolBlocksSubscription?.unsubscribe();
this.flowPrefSubscription?.unsubscribe();
this.stateService.markBlock$.next({});
this.broadcastSubscription?.unsubscribe();
}
}

View File

@ -16,6 +16,11 @@
<div class="header-bg box" [attr.data-cy]="'tx-' + i">
<div *ngIf="errorUnblinded" class="error-unblinded">{{ errorUnblinded }}</div>
@if (similarityMatches.get(tx.txid)?.size) {
<div class="alert alert-mempool" role="alert">
<span i18n="transaction.poison.warning">Warning! This transaction involves deceptively similar addresses. It may be an address poisoning attack.</span>
</div>
}
<div class="row">
<div class="col">
<table class="table table-fixed table-borderless smaller-text table-sm table-tx-vin">
@ -68,9 +73,11 @@
</ng-template>
</ng-template>
<ng-template #defaultAddress>
<a class="address" *ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
<app-truncate [text]="vin.prevout.scriptpubkey_address" [lastChars]="8"></app-truncate>
</a>
<app-address-text
*ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType"
[address]="vin.prevout.scriptpubkey_address"
[similarity]="similarityMatches.get(tx.txid)?.get(vin.prevout.scriptpubkey_address)"
></app-address-text>
<ng-template #vinScriptPubkeyType>
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
</ng-template>
@ -217,9 +224,11 @@
'highlight': this.addresses.length && ((vout.scriptpubkey_type !== 'p2pk' && addresses.includes(vout.scriptpubkey_address)) || this.addresses.includes(vout.scriptpubkey.slice(2, -2)))
}">
<td class="address-cell">
<a class="address" *ngIf="vout.scriptpubkey_address; else pubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
<app-truncate [text]="vout.scriptpubkey_address" [lastChars]="8"></app-truncate>
</a>
<app-address-text
*ngIf="vout.scriptpubkey_address; else pubkey_type"
[address]="vout.scriptpubkey_address"
[similarity]="similarityMatches.get(tx.txid)?.get(vout.scriptpubkey_address)"
></app-address-text>
<ng-template #pubkey_type>
<ng-container *ngIf="vout.scriptpubkey_type === 'p2pk'; else scriptpubkey_type">
P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey.slice(2, -2)]" title="{{ vout.scriptpubkey.slice(2, -2) }}">

View File

@ -14,6 +14,7 @@ import { StorageService } from '@app/services/storage.service';
import { OrdApiService } from '@app/services/ord-api.service';
import { Inscription } from '@app/shared/ord/inscription.utils';
import { Etching, Runestone } from '@app/shared/ord/rune.utils';
import { ADDRESS_SIMILARITY_THRESHOLD, AddressMatch, AddressSimilarity, AddressType, AddressTypeInfo, checkedCompareAddressStrings, detectAddressType } from '@app/shared/address-utils';
@Component({
selector: 'app-transactions-list',
@ -55,6 +56,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
showFullScript: { [vinIndex: number]: boolean } = {};
showFullWitness: { [vinIndex: number]: { [witnessIndex: number]: boolean } } = {};
showOrdData: { [key: string]: { show: boolean; inscriptions?: Inscription[]; runestone?: Runestone, runeInfo?: { [id: string]: { etching: Etching; txid: string; } }; } } = {};
similarityMatches: Map<string, Map<string, { score: number, match: AddressMatch, group: number }>> = new Map();
constructor(
public stateService: StateService,
@ -144,6 +146,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
this.currency = currency;
this.refreshPrice();
});
this.updateAddressSimilarities();
}
refreshPrice(): void {
@ -183,6 +187,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
}
}
if (changes.transactions || changes.addresses) {
this.similarityMatches.clear();
this.updateAddressSimilarities();
if (!this.transactions || !this.transactions.length) {
return;
}
@ -296,6 +302,56 @@ export class TransactionsListComponent implements OnInit, OnChanges {
}
}
updateAddressSimilarities(): void {
if (!this.transactions || !this.transactions.length) {
return;
}
for (const tx of this.transactions) {
if (this.similarityMatches.get(tx.txid)) {
continue;
}
const similarityGroups: Map<string, number> = new Map();
let lastGroup = 0;
// Check for address poisoning similarity matches
this.similarityMatches.set(tx.txid, new Map());
const comparableVouts = [
...tx.vout.slice(0, 20),
...this.addresses.map(addr => ({ scriptpubkey_address: addr, scriptpubkey_type: detectAddressType(addr, this.stateService.network) }))
].filter(v => ['p2pkh', 'p2sh', 'v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(v.scriptpubkey_type));
const comparableVins = tx.vin.slice(0, 20).map(v => v.prevout).filter(v => ['p2pkh', 'p2sh', 'v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(v?.scriptpubkey_type));
for (const vout of comparableVouts) {
const address = vout.scriptpubkey_address;
const addressType = vout.scriptpubkey_type;
if (this.similarityMatches.get(tx.txid)?.has(address)) {
continue;
}
for (const compareAddr of [
...comparableVouts.filter(v => v.scriptpubkey_type === addressType && v.scriptpubkey_address !== address),
...comparableVins.filter(v => v.scriptpubkey_type === addressType && v.scriptpubkey_address !== address)
]) {
const similarity = checkedCompareAddressStrings(address, compareAddr.scriptpubkey_address, addressType as AddressType, this.stateService.network);
if (similarity?.status === 'comparable' && similarity.score > ADDRESS_SIMILARITY_THRESHOLD) {
let group = similarityGroups.get(address) || lastGroup++;
similarityGroups.set(address, group);
const bestVout = this.similarityMatches.get(tx.txid)?.get(address);
if (!bestVout || bestVout.score < similarity.score) {
this.similarityMatches.get(tx.txid)?.set(address, { score: similarity.score, match: similarity.left, group });
}
// opportunistically update the entry for the compared address
const bestCompare = this.similarityMatches.get(tx.txid)?.get(compareAddr.scriptpubkey_address);
if (!bestCompare || bestCompare.score < similarity.score) {
group = similarityGroups.get(compareAddr.scriptpubkey_address) || lastGroup++;
similarityGroups.set(compareAddr.scriptpubkey_address, group);
this.similarityMatches.get(tx.txid)?.set(compareAddr.scriptpubkey_address, { score: similarity.score, match: similarity.right, group });
}
}
}
}
}
}
onScroll(): void {
this.loadMore.emit();
}

View File

@ -11803,7 +11803,7 @@ export const restApiDocsData = [
fragment: "accelerator-cancel",
title: "POST Cancel Acceleration (Pro)",
description: {
default: "<p>Sends a request to cancel an acceleration in the <code>accelerating</code> status.<br>You can retreive eligible acceleration <code>id</code> using the history endpoint GET <code>/api/v1/services/accelerator/history?status=accelerating</code>."
default: "<p>Sends a request to cancel an acceleration in the <code>accelerating</code> status.<br>You can retrieve eligible acceleration <code>id</code> using the history endpoint GET <code>/api/v1/services/accelerator/history?status=accelerating</code>."
},
urlString: "/v1/services/accelerator/cancel",
showConditions: [""],

View File

@ -219,7 +219,7 @@
</ng-template>
<ng-template type="how-to-get-transaction-confirmed-quickly">
<p>To get your transaction confirmed quicker, you will need to increase its effective feerate.</p><p>If your transaction was created with RBF enabled, your stuck transaction can simply be replaced with a new one that has a higher fee. Otherwise, if you control any of the stuck transaction's outputs, you can use CPFP to increase your stuck transaction's effective feerate.</p><p>If you are not sure how to do RBF or CPFP, work with the tool you used to make the transaction (wallet software, exchange company, etc).</p><p>Another option to get your transaction confirmed more quickly is <a [href]="[ isMempoolSpaceBuild ? '/accelerator' : 'https://mempool.space/accelerator']" [target]="isMempoolSpaceBuild ? '' : 'blank'">Mempool Accelerator</a>.</p>
<p>To get your transaction confirmed quicker, you will need to increase its effective feerate.</p><p>If your transaction was created with RBF enabled, your stuck transaction can simply be replaced with a new one that has a higher fee. Otherwise, if you control any of the stuck transaction's outputs, you can use CPFP to increase your stuck transaction's effective feerate.</p><p>If you are not sure how to do RBF or CPFP, work with the tool you used to make the transaction (wallet software, exchange company, etc).</p><p>Another option to get your transaction confirmed more quickly is <a [href]="[ isMempoolSpaceBuild ? '/accelerator' : 'https://mempool.space/accelerator']" [target]="isMempoolSpaceBuild ? '' : 'blank'">Mempool Accelerator®</a>.</p>
</ng-template>
<ng-template type="how-prevent-stuck-transaction">

View File

@ -1,7 +1,7 @@
// Import tree-shakeable echarts
import * as echarts from 'echarts/core';
import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart, CustomChart } from 'echarts/charts';
import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent } from 'echarts/components';
import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent, GraphicComponent } from 'echarts/components';
import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
// Typescript interfaces
import { EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption } from 'echarts';
@ -13,6 +13,6 @@ echarts.use([
LegendComponent, GeoComponent, DataZoomComponent,
VisualMapComponent, MarkLineComponent,
LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart,
CustomChart,
CustomChart, GraphicComponent
]);
export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption };

View File

@ -26,7 +26,6 @@ import { StatisticsComponent } from '@components/statistics/statistics.component
import { MempoolBlockComponent } from '@components/mempool-block/mempool-block.component';
import { PoolRankingComponent } from '@components/pool-ranking/pool-ranking.component';
import { PoolComponent } from '@components/pool/pool.component';
import { TelevisionComponent } from '@components/television/television.component';
import { DashboardComponent } from '@app/dashboard/dashboard.component';
import { CustomDashboardComponent } from '@components/custom-dashboard/custom-dashboard.component';
import { MiningDashboardComponent } from '@components/mining-dashboard/mining-dashboard.component';
@ -56,7 +55,6 @@ import { CommonModule } from '@angular/common';
AcceleratorDashboardComponent,
PoolComponent,
PoolRankingComponent,
TelevisionComponent,
StatisticsComponent,
GraphsComponent,

View File

@ -16,7 +16,6 @@ import { PoolRankingComponent } from '@components/pool-ranking/pool-ranking.comp
import { PoolComponent } from '@components/pool/pool.component';
import { StartComponent } from '@components/start/start.component';
import { StatisticsComponent } from '@components/statistics/statistics.component';
import { TelevisionComponent } from '@components/television/television.component';
import { DashboardComponent } from '@app/dashboard/dashboard.component';
import { CustomDashboardComponent } from '@components/custom-dashboard/custom-dashboard.component';
import { AccelerationFeesGraphComponent } from '@components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component';
@ -180,11 +179,6 @@ const routes: Routes = [
},
]
},
{
path: 'tv',
data: { networks: ['bitcoin', 'liquid'] },
component: TelevisionComponent
},
];
@NgModule({

View File

@ -213,7 +213,7 @@ export class ServicesApiServices {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/payments/bitcoin`, params);
}
retreiveInvoice$(invoiceId: string): Observable<any[]> {
retrieveInvoice$(invoiceId: string): Observable<any[]> {
return this.httpClient.get<any[]>(`${this.stateService.env.SERVICES_API}/payments/bitcoin/invoice?id=${invoiceId}`);
}

View File

@ -78,6 +78,7 @@ const p2trRegex = RegExp('^p' + BECH32_CHARS_LW + '{58}$');
const pubkeyRegex = RegExp('^' + `(04${HEX_CHARS}{128})|(0[23]${HEX_CHARS}{64})$`);
export function detectAddressType(address: string, network: string): AddressType {
network = network || 'mainnet';
// normal address types
const firstChar = address.substring(0, 1);
if (ADDRESS_PREFIXES[network].base58.pubkey.includes(firstChar) && base58Regex.test(address.slice(1))) {
@ -211,6 +212,18 @@ export class AddressTypeInfo {
}
}
public compareTo(other: AddressTypeInfo): AddressSimilarityResult {
return compareAddresses(this.address, other.address, this.network);
}
public compareToString(other: string): AddressSimilarityResult {
if (other === this.address) {
return { status: 'identical' };
}
const otherInfo = new AddressTypeInfo(this.network, other);
return this.compareTo(otherInfo);
}
private processScript(script: ScriptInfo): void {
this.scripts.set(script.key, script);
if (script.template?.type === 'multisig') {
@ -218,3 +231,135 @@ export class AddressTypeInfo {
}
}
}
export interface AddressMatch {
prefix: string;
postfix: string;
}
export interface AddressSimilarity {
status: 'comparable';
score: number;
left: AddressMatch;
right: AddressMatch;
}
export type AddressSimilarityResult =
| { status: 'identical' }
| { status: 'incomparable' }
| AddressSimilarity;
export const ADDRESS_SIMILARITY_THRESHOLD = 10_000_000; // 1 false positive per ~10 million comparisons
function fuzzyPrefixMatch(a: string, b: string, rtl: boolean = false): { score: number, matchA: string, matchB: string } {
let score = 0;
let gap = false;
let done = false;
let ai = 0;
let bi = 0;
let prefixA = '';
let prefixB = '';
if (rtl) {
a = a.split('').reverse().join('');
b = b.split('').reverse().join('');
}
while (ai < a.length && bi < b.length && !done) {
if (a[ai] === b[bi]) {
// matching characters
prefixA += a[ai];
prefixB += b[bi];
score++;
ai++;
bi++;
} else if (!gap) {
// try looking ahead in both strings to find the best match
const nextMatchA = (ai + 1 < a.length && a[ai + 1] === b[bi]);
const nextMatchB = (bi + 1 < b.length && a[ai] === b[bi + 1]);
const nextMatchBoth = (ai + 1 < a.length && bi + 1 < b.length && a[ai + 1] === b[bi + 1]);
if (nextMatchBoth) {
// single differing character
prefixA += a[ai];
prefixB += b[bi];
ai++;
bi++;
} else if (nextMatchA) {
// character missing in b
prefixA += a[ai];
ai++;
} else if (nextMatchB) {
// character missing in a
prefixB += b[bi];
bi++;
} else {
ai++;
bi++;
}
gap = true;
} else {
done = true;
}
}
if (rtl) {
prefixA = prefixA.split('').reverse().join('');
prefixB = prefixB.split('').reverse().join('');
}
return { score, matchA: prefixA, matchB: prefixB };
}
export function compareAddressInfo(a: AddressTypeInfo, b: AddressTypeInfo): AddressSimilarityResult {
if (a.address === b.address) {
return { status: 'identical' };
}
if (a.type !== b.type) {
return { status: 'incomparable' };
}
if (!['p2pkh', 'p2sh', 'p2sh-p2wpkh', 'p2sh-p2wsh', 'v0_p2wpkh', 'v0_p2wsh', 'v1_p2tr'].includes(a.type)) {
return { status: 'incomparable' };
}
const isBase58 = a.type === 'p2pkh' || a.type === 'p2sh';
const left = fuzzyPrefixMatch(a.address, b.address);
const right = fuzzyPrefixMatch(a.address, b.address, true);
// depending on address type, some number of matching prefix characters are guaranteed
const prefixScore = isBase58 ? 1 : ADDRESS_PREFIXES[a.network || 'mainnet'].bech32.length;
// add the two scores together
const totalScore = left.score + right.score - prefixScore;
// adjust for the size of the alphabet (58 vs 32)
const normalizedScore = Math.pow(isBase58 ? 58 : 32, totalScore);
return {
status: 'comparable',
score: normalizedScore,
left: {
prefix: left.matchA,
postfix: right.matchA,
},
right: {
prefix: left.matchB,
postfix: right.matchB,
},
};
}
export function compareAddresses(a: string, b: string, network: string): AddressSimilarityResult {
if (a === b) {
return { status: 'identical' };
}
const aInfo = new AddressTypeInfo(network, a);
return aInfo.compareToString(b);
}
// avoids the overhead of creating AddressTypeInfo objects for each address,
// but a and b *MUST* be valid normalized addresses, of the same valid type
export function checkedCompareAddressStrings(a: string, b: string, type: AddressType, network: string): AddressSimilarityResult {
return compareAddressInfo(
{ address: a, type: type, network: network } as AddressTypeInfo,
{ address: b, type: type, network: network } as AddressTypeInfo,
);
}

View File

@ -0,0 +1,17 @@
@if (similarity) {
<div class="address-text">
<a class="address" style="display: contents;" [routerLink]="['/address/' | relativeUrl, address]" title="{{ address }}">
<span class="prefix">{{ similarity.match.prefix }}</span>
<span class="infix" [ngStyle]="{'text-decoration-color': groupColors[similarity.group % (groupColors.length)]}">{{ address.slice(similarity.match.prefix.length || 0, -similarity.match.postfix.length || undefined) }}</span>
<span class="postfix"> {{ similarity.match.postfix }}</span>
</a>
<span class="poison-alert" *ngIf="similarity" i18n-ngbTooltip="address-poisoning.warning-tooltip" ngbTooltip="This address is deceptively similar to another output. It may be part of an address poisoning attack.">
<fa-icon [icon]="['fas', 'exclamation-triangle']" [fixedWidth]="true"></fa-icon>
</span>
</div>
} @else {
<a class="address" [routerLink]="['/address/' | relativeUrl, address]" title="{{ address }}">
<app-truncate [text]="address" [lastChars]="8"></app-truncate>
</a>
}

View File

@ -0,0 +1,32 @@
.address-text {
text-overflow: unset;
display: flex;
flex-direction: row;
align-items: start;
position: relative;
font-family: monospace;
.infix {
flex-grow: 0;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
user-select: none;
text-decoration: underline 2px;
}
.prefix, .postfix {
flex-shrink: 0;
flex-grow: 0;
user-select: none;
text-decoration: underline var(--red) 2px;
}
}
.poison-alert {
margin-left: .5em;
color: var(--yellow);
}

View File

@ -0,0 +1,20 @@
import { Component, Input } from '@angular/core';
import { AddressMatch, AddressTypeInfo } from '@app/shared/address-utils';
@Component({
selector: 'app-address-text',
templateUrl: './address-text.component.html',
styleUrls: ['./address-text.component.scss']
})
export class AddressTextComponent {
@Input() address: string;
@Input() info: AddressTypeInfo | null;
@Input() similarity: { score: number, match: AddressMatch, group: number } | null;
groupColors: string[] = [
'var(--primary)',
'var(--success)',
'var(--info)',
'white',
];
}

View File

@ -256,6 +256,11 @@ export function detectScriptTemplate(type: ScriptType, script_asm: string, witne
return ScriptTemplates.multisig(tapscriptMultisig.m, tapscriptMultisig.n);
}
const tapscriptUnanimousMultisig = parseTapscriptUnanimousMultisig(script_asm);
if (tapscriptUnanimousMultisig) {
return ScriptTemplates.multisig(tapscriptUnanimousMultisig, tapscriptUnanimousMultisig);
}
return;
}
@ -310,11 +315,13 @@ export function parseTapscriptMultisig(script: string): undefined | { m: number,
}
const ops = script.split(' ');
// At minimum, one pubkey group (3 tokens) + m push + final opcode = 5 tokens
if (ops.length < 5) return;
// At minimum, 2 pubkey group (3 tokens) + m push + final opcode = 8 tokens
if (ops.length < 8) {
return;
}
const finalOp = ops.pop();
if (finalOp !== 'OP_NUMEQUAL' && finalOp !== 'OP_GREATERTHANOREQUAL') {
if (!['OP_NUMEQUAL', 'OP_NUMEQUALVERIFY', 'OP_GREATERTHANOREQUAL', 'OP_GREATERTHAN', 'OP_EQUAL', 'OP_EQUALVERIFY'].includes(finalOp)) {
return;
}
@ -329,6 +336,10 @@ export function parseTapscriptMultisig(script: string): undefined | { m: number,
return;
}
if (finalOp === 'OP_GREATERTHAN') {
m += 1;
}
if (ops.length % 3 !== 0) {
return;
}
@ -360,6 +371,53 @@ export function parseTapscriptMultisig(script: string): undefined | { m: number,
return { m, n };
}
export function parseTapscriptUnanimousMultisig(script: string): undefined | number {
if (!script) {
return;
}
const ops = script.split(' ');
// At minimum, 2 pubkey group (3 tokens) = 6 tokens
if (ops.length < 6) {
return;
}
if (ops.length % 3 !== 0) {
return;
}
const n = ops.length / 3;
for (let i = 0; i < n; i++) {
const pushOp = ops.shift();
const pubkey = ops.shift();
const sigOp = ops.shift();
if (pushOp !== 'OP_PUSHBYTES_32') {
return;
}
if (!/^[0-9a-fA-F]{64}$/.test(pubkey)) {
return;
}
if (i < n - 1) {
if (sigOp !== 'OP_CHECKSIGVERIFY') {
return;
}
} else {
// Last opcode can be either CHECKSIG or CHECKSIGVERIFY
if (!(sigOp === 'OP_CHECKSIGVERIFY' || sigOp === 'OP_CHECKSIG')) {
return;
}
}
}
if (ops.length) {
return;
}
return n;
}
export function getVarIntLength(n: number): number {
if (n < 0xfd) {
return 1;

View File

@ -3,11 +3,11 @@ import { CommonModule } from '@angular/common';
import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
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, faMicroscope } from '@fortawesome/free-solid-svg-icons';
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard, faMicroscope, faExclamationTriangle } 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';
@ -94,6 +94,7 @@ import { SatsComponent } from '@app/shared/components/sats/sats.component';
import { BtcComponent } from '@app/shared/components/btc/btc.component';
import { FeeRateComponent } from '@app/shared/components/fee-rate/fee-rate.component';
import { AddressTypeComponent } from '@app/shared/components/address-type/address-type.component';
import { AddressTextComponent } from '@app/shared/components/address-text/address-text.component';
import { TruncateComponent } from '@app/shared/components/truncate/truncate.component';
import { SearchResultsComponent } from '@components/search-form/search-results/search-results.component';
import { TimestampComponent } from '@app/shared/components/timestamp/timestamp.component';
@ -214,6 +215,7 @@ import { GithubLogin } from '../components/github-login.component/github-login.c
BtcComponent,
FeeRateComponent,
AddressTypeComponent,
AddressTextComponent,
TruncateComponent,
SearchResultsComponent,
TimestampComponent,
@ -360,6 +362,7 @@ import { GithubLogin } from '../components/github-login.component/github-login.c
BtcComponent,
FeeRateComponent,
AddressTypeComponent,
AddressTextComponent,
TruncateComponent,
SearchResultsComponent,
TimestampComponent,
@ -395,7 +398,6 @@ export class SharedModule {
constructor(library: FaIconLibrary) {
library.addIcons(faInfoCircle);
library.addIcons(faChartArea);
library.addIcons(faTv);
library.addIcons(faClock);
library.addIcons(faTachometerAlt);
library.addIcons(faCubes);
@ -465,5 +467,6 @@ export class SharedModule {
library.addIcons(faShareNodes);
library.addIcons(faCreditCard);
library.addIcons(faMicroscope);
library.addIcons(faExclamationTriangle);
}
}

View File

@ -1567,7 +1567,7 @@
</trans-unit>
<trans-unit id="bdb8bbb38e4ca3c73e19dc4167fbe4aec316f818" datatype="html">
<source>Total Bid Boost</source>
<target>Augmentation totale des frais</target>
<target>Total frais ajoutés</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/acceleration/acceleration-stats/acceleration-stats.component.html</context>
<context context-type="linenumber">11</context>
@ -1728,7 +1728,7 @@
</trans-unit>
<trans-unit id="57cde27765d527a0d9195212fa5a7ce06408c827" datatype="html">
<source>Bid Boost</source>
<target>Augmentation des frais</target>
<target>Frais ajoutés</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/acceleration/accelerations-list/accelerations-list.component.html</context>
<context context-type="linenumber">17</context>
@ -6739,7 +6739,7 @@
</trans-unit>
<trans-unit id="date-base.just-now" datatype="html">
<source>Just now</source>
<target>Juste maintenant</target>
<target>À l'instant</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/time/time.component.ts</context>
<context context-type="linenumber">111</context>
@ -9847,7 +9847,7 @@
</trans-unit>
<trans-unit id="8e623d3cfecb7c560c114390db53c1f430ffd0de" datatype="html">
<source><x id="INTERPOLATION" equiv-text="{{ i }}"/> confirmation</source>
<target><x id="INTERPOLATION" equiv-text="{{ i }}"/>confirmation</target>
<target><x id="INTERPOLATION" equiv-text="{{ i }}"/> confirmation</target>
<context-group purpose="location">
<context context-type="sourcefile">src/app/shared/components/confirmations/confirmations.component.html</context>
<context context-type="linenumber">4</context>

View File

@ -84,11 +84,11 @@ pkg install -y zsh sudo git screen curl wget neovim rsync nginx openssl openssh-
### Node.js + npm
Build Node.js v20.17.0 and npm v9 from source using `nvm`:
Build Node.js v22.14.0 and npm v9 from source using `nvm`:
```
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | zsh
source $HOME/.zshrc
nvm install v20.17.0 --shared-zlib
nvm install v22.14.0 --shared-zlib
nvm alias default node
```

View File

@ -378,7 +378,7 @@ ELEMENTS_REPO_URL=https://github.com/ElementsProject/elements
ELEMENTS_REPO_NAME=elements
ELEMENTS_REPO_BRANCH=master
#ELEMENTS_LATEST_RELEASE=$(curl -s https://api.github.com/repos/ElementsProject/elements/releases/latest|grep tag_name|head -1|cut -d '"' -f4)
ELEMENTS_LATEST_RELEASE=elements-23.2.6
ELEMENTS_LATEST_RELEASE=elements-23.2.7
echo -n '.'
BITCOIN_ELECTRS_REPO_URL=https://github.com/mempool/electrs
@ -1116,8 +1116,8 @@ echo "[*] Installing nvm.sh from GitHub"
osSudo "${MEMPOOL_USER}" sh -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh'
echo "[*] Building NodeJS via nvm.sh"
osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm install v20.12.0 --shared-zlib'
osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm alias default 20.12.0'
osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm install v22.14.0 --shared-zlib'
osSudo "${MEMPOOL_USER}" zsh -c 'source ~/.zshrc ; nvm alias default 22.14.0'
####################
# Tor installation #
@ -1565,7 +1565,7 @@ EOF
osSudo "${UNFURL_USER}" sh -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh'
echo "[*] Building NodeJS via nvm.sh"
osSudo "${UNFURL_USER}" zsh -c 'source ~/.zshrc ; nvm install v20.12.0 --shared-zlib'
osSudo "${UNFURL_USER}" zsh -c 'source ~/.zshrc ; nvm install v22.14.0 --shared-zlib'
;;
esac

View File

@ -159,6 +159,7 @@
},
"WALLETS": {
"ENABLED": true,
"AUTO": true,
"WALLETS": ["BITB", "3350"]
},
"STRATUM": {

View File

@ -1,7 +1,7 @@
#!/usr/bin/env zsh
export NVM_DIR="$HOME/.nvm"
source "$NVM_DIR/nvm.sh"
nvm use v20.12.0
nvm use v22.14.0
# start all mempool backends that exist
for site in mainnet mainnet-lightning testnet testnet-lightning testnet4 signet signet-lightning liquid liquidtestnet;do