Compare commits

..

40 Commits

Author SHA1 Message Date
dependabot[bot]
8bf21c2fb9 Bump ws and puppeteer in /unfurler
Bumps [ws](https://github.com/websockets/ws) to 8.17.1 and updates ancestor dependency [puppeteer](https://github.com/puppeteer/puppeteer). These dependencies need to be updated together.


Updates `ws` from 8.8.0 to 8.17.1
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.8.0...8.17.1)

Updates `puppeteer` from 15.3.2 to 22.11.2
- [Release notes](https://github.com/puppeteer/puppeteer/releases)
- [Changelog](https://github.com/puppeteer/puppeteer/blob/main/release-please-config.json)
- [Commits](https://github.com/puppeteer/puppeteer/compare/v15.3.2...puppeteer-v22.11.2)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
- dependency-name: puppeteer
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-19 03:01:31 +00:00
softsimon
0c8d9daaec Merge pull request #5165 from mempool/dependabot/npm_and_yarn/backend/ws-8.17.1
Bump ws from 8.17.0 to 8.17.1 in /backend
2024-06-19 12:00:46 +09:00
softsimon
307d3627a0 Merge pull request #5164 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.12.0
Bump cypress from 13.11.0 to 13.12.0 in /frontend
2024-06-19 12:00:30 +09:00
dependabot[bot]
db04c4663e Bump ws from 8.17.0 to 8.17.1 in /backend
Bumps [ws](https://github.com/websockets/ws) from 8.17.0 to 8.17.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.17.0...8.17.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-19 02:27:59 +00:00
dependabot[bot]
a0a6a0da4f Bump cypress from 13.11.0 to 13.12.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.11.0 to 13.12.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v13.11.0...v13.12.0)

---
updated-dependencies:
- dependency-name: cypress
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-19 02:21:54 +00:00
wiz
ccd8412e6a Merge pull request #5157 from mempool/nymkappa/grumpy
[about page] use grumpy guy instead of boring placeholder
2024-06-17 11:12:26 +09:00
nymkappa
84dae82e90 [about page] use grumpy guy instead of boring placeholder 2024-06-15 18:50:27 +09:00
softsimon
684ad9f0e6 Merge pull request #5062 from mempool/mononaut/configurable-tip-monitoring
Configurable threshold for esplora tip check
2024-06-10 00:52:50 +04:00
softsimon
24b5d4e971 Fix docker default value 2024-06-10 00:52:39 +04:00
softsimon
fda40cad48 Fix trailing comma 2024-06-10 00:47:40 +04:00
softsimon
2ce4b5604e Merge pull request #5130 from mempool/dependabot/docker/docker/frontend/node-20.14.0-buster-slim
Bump node from 20.13.1-buster-slim to 20.14.0-buster-slim in /docker/frontend
2024-06-10 00:16:42 +04:00
softsimon
fb660e8477 Merge pull request #5129 from mempool/dependabot/docker/docker/frontend/nginx-1.27.0-alpine
Bump nginx from 1.26.0-alpine to 1.27.0-alpine in /docker/frontend
2024-06-10 00:16:34 +04:00
softsimon
621def712d Merge pull request #5128 from mempool/dependabot/docker/docker/backend/node-20.14.0-buster-slim
Bump node from 20.13.1-buster-slim to 20.14.0-buster-slim in /docker/backend
2024-06-10 00:16:21 +04:00
softsimon
8382a27a7c Merge pull request #5149 from mempool/mononaut/accurate-timestamps-hover
Accurate timestamps on hover
2024-06-10 00:16:07 +04:00
softsimon
b7d96a2a26 Merge pull request #5145 from mempool/natsoni/tapscript-toggle-show-more
Refactor "show all" toggle for long witnesses and witness scripts
2024-06-10 00:11:26 +04:00
Mononaut
3149199c8a Accurate timestamps on hover 2024-06-08 23:28:44 +00:00
softsimon
0c3ef4eabc Merge pull request #5139 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.11.0
Bump cypress from 13.10.0 to 13.11.0 in /frontend
2024-06-08 19:12:02 +04:00
wiz
fffcb5038f Merge pull request #5136 from mempool/mononaut/research
research footer link
2024-06-07 11:49:51 +09:00
natsoni
77d42bfdbb Don't render full input witness if user does not press "show all" 2024-06-06 17:53:20 +02:00
natsoni
f840ac951b Add show all toggle for redeem scripts 2024-06-06 11:43:21 +02:00
wiz
22a48efd19 Merge pull request #5141 from mempool/nymkappa/liquid-fix
[liquid] don't fetch pools
2024-06-05 15:31:44 +09:00
nymkappa
fba3f7ec1c [liquid] don't fetch pools 2024-06-05 08:28:01 +02:00
wiz
6b3005c49d Merge pull request #5125 from mempool/mononaut/recent-address-chart
Make address chart prefer "recent" mode by default
2024-06-05 14:22:00 +09:00
wiz
17132ff047 Merge pull request #5120 from mempool/mononaut/multi-pool-eta
Multi-pool ETA
2024-06-05 14:21:34 +09:00
wiz
355fe58b43 Merge pull request #5137 from mempool/mononaut/pizza-replacement
Pizza tracker RBF support
2024-06-05 14:20:51 +09:00
wiz
604b0ba3e6 Merge pull request #5135 from mempool/mononaut/research-images
research images
2024-06-05 14:19:34 +09:00
dependabot[bot]
d016838356 Bump cypress from 13.10.0 to 13.11.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.10.0 to 13.11.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v13.10.0...v13.11.0)

---
updated-dependencies:
- dependency-name: cypress
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-05 02:38:18 +00:00
Mononaut
f77582250f Pizza tracker rbf tree traversal to find mined tx 2024-06-04 23:02:13 +00:00
Mononaut
976e505445 Pizza tracker handle RBF replacements 2024-06-04 22:45:43 +00:00
Mononaut
42c60fd991 Defer db access to fix failing tests 2024-06-04 20:57:40 +00:00
Mononaut
9a838c7269 Use estimated acceleration positions in frontend 2024-06-04 20:40:41 +00:00
Mononaut
f31b28251c Estimate accelerated positions in partner mempools 2024-06-04 20:40:40 +00:00
Mononaut
ced1595d70 research footer link 2024-06-04 15:50:13 +00:00
Mononaut
0b0109d821 Research unfurler preview image 2024-06-04 15:25:04 +00:00
Mononaut
992da1e5d2 Research logo 2024-06-04 15:09:45 +00:00
dependabot[bot]
9d4bbe9317 Bump node in /docker/frontend
Bumps node from 20.13.1-buster-slim to 20.14.0-buster-slim.

---
updated-dependencies:
- dependency-name: node
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 02:53:03 +00:00
dependabot[bot]
5575798cb6 Bump nginx from 1.26.0-alpine to 1.27.0-alpine in /docker/frontend
Bumps nginx from 1.26.0-alpine to 1.27.0-alpine.

---
updated-dependencies:
- dependency-name: nginx
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 02:52:57 +00:00
dependabot[bot]
57cc53b64e Bump node in /docker/backend
Bumps node from 20.13.1-buster-slim to 20.14.0-buster-slim.

---
updated-dependencies:
- dependency-name: node
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-03 02:17:00 +00:00
Mononaut
37725bb341 Make address graph prefer "recent" mode by default 2024-05-31 17:20:07 +00:00
Mononaut
568084e143 Configurable threshold for esplora tip check 2024-05-12 00:35:25 +00:00
57 changed files with 2247 additions and 543 deletions

View File

@@ -59,7 +59,8 @@
"RETRY_UNIX_SOCKET_AFTER": 30000,
"REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000,
"FALLBACK": []
"FALLBACK": [],
"MAX_BEHIND_TIP": 2
},
"SECOND_CORE_RPC": {
"HOST": "127.0.0.1",

View File

@@ -23,7 +23,7 @@
"rust-gbt": "file:./rust-gbt",
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3",
"ws": "~8.17.0"
"ws": "~8.17.1"
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",
@@ -7690,9 +7690,9 @@
}
},
"node_modules/ws": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},
@@ -13424,9 +13424,9 @@
}
},
"ws": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"requires": {}
},
"y18n": {

View File

@@ -52,7 +52,7 @@
"redis": "^4.6.6",
"socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3",
"ws": "~8.17.0"
"ws": "~8.17.1"
},
"devDependencies": {
"@babel/code-frame": "^7.18.6",

View File

@@ -60,7 +60,8 @@
"RETRY_UNIX_SOCKET_AFTER": 888,
"REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000,
"FALLBACK": []
"FALLBACK": [],
"MAX_BEHIND_TIP": 2
},
"SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__",

View File

@@ -63,6 +63,7 @@ describe('Mempool Backend Config', () => {
REQUEST_TIMEOUT: 10000,
FALLBACK_TIMEOUT: 5000,
FALLBACK: [],
MAX_BEHIND_TIP: 2,
});
expect(config.CORE_RPC).toStrictEqual({

View File

@@ -25,6 +25,7 @@ interface FailoverHost {
class FailoverRouter {
activeHost: FailoverHost;
fallbackHost: FailoverHost;
maxSlippage: number = config.ESPLORA.MAX_BEHIND_TIP ?? 2;
maxHeight: number = 0;
hosts: FailoverHost[];
multihost: boolean;
@@ -93,13 +94,13 @@ class FailoverRouter {
);
if (result) {
const height = result.data;
this.maxHeight = Math.max(height, this.maxHeight);
this.maxHeight = Math.max(height || 0, ...this.hosts.map(host => (!(host.unreachable || host.timedOut || host.outOfSync) ? host.latestHeight || 0 : 0)));
const rtt = result.config['meta'].rtt;
host.rtts.unshift(rtt);
host.rtts.slice(0, 5);
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
host.latestHeight = height;
if (height == null || isNaN(height) || (this.maxHeight - height > 2)) {
if (height == null || isNaN(height) || (this.maxHeight - height > this.maxSlippage)) {
host.outOfSync = true;
} else {
host.outOfSync = false;

View File

@@ -1,12 +1,13 @@
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from 'rust-gbt';
import logger from '../logger';
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, TransactionClassified, TransactionCompressed, MempoolDeltaChange, GbtCandidates } from '../mempool.interfaces';
import { MempoolBlock, MempoolTransactionExtended, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, TransactionClassified, TransactionCompressed, MempoolDeltaChange, GbtCandidates, PoolTag } from '../mempool.interfaces';
import { Common, OnlineFeeStatsCalculator } from './common';
import config from '../config';
import { Worker } from 'worker_threads';
import path from 'path';
import mempool from './mempool';
import { Acceleration } from './services/acceleration';
import PoolsRepository from '../repositories/PoolsRepository';
const MAX_UINT32 = Math.pow(2, 32) - 1;
@@ -21,6 +22,8 @@ class MempoolBlocks {
private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
private txidMap: Map<string, number> = new Map(); // map full txids back to short numerical uids
private pools: { [id: number]: PoolTag } = {};
public getMempoolBlocks(): MempoolBlock[] {
return this.mempoolBlocks.map((block) => {
return {
@@ -42,6 +45,18 @@ class MempoolBlocks {
return this.mempoolBlockDeltas;
}
public async updatePools$(): Promise<void> {
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
this.pools = {};
return;
}
const allPools = await PoolsRepository.$getPools();
this.pools = {};
for (const pool of allPools) {
this.pools[pool.uniqueId] = pool;
}
}
private calculateMempoolDeltas(prevBlocks: MempoolBlockWithTransactions[], mempoolBlocks: MempoolBlockWithTransactions[]): MempoolBlockDelta[] {
const mempoolBlockDeltas: MempoolBlockDelta[] = [];
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
@@ -478,7 +493,7 @@ class MempoolBlocks {
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
this.mempoolBlocks = mempoolBlocks;
this.mempoolBlockDeltas = deltas;
this.updateAccelerationPositions(mempool, accelerations, mempoolBlocks);
}
return mempoolBlocks;
@@ -625,6 +640,124 @@ class MempoolBlocks {
tx.acc ? 1 : 0,
];
}
// estimates and saves positions of accelerations in mining partner mempools
private updateAccelerationPositions(mempoolCache: { [txid: string]: MempoolTransactionExtended }, accelerations: { [txid: string]: Acceleration }, mempoolBlocks: MempoolBlockWithTransactions[]): void {
const accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
// keep track of simulated mempool blocks for each active pool
const pools: {
[pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean };
} = {};
// prepare a list of accelerations in ascending order (we'll pop items off the end of the list)
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).map(acc => {
let vsize = mempoolCache[acc.txid].vsize;
for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
vsize += (ancestor.weight / 4);
}
return {
acceleration: acc,
rate: mempoolCache[acc.txid].effectiveFeePerVsize,
vsize
};
}).sort((a, b) => a.rate - b.rate);
// initialize the pool tracker
for (const { acceleration } of accQueue) {
accelerationPositions[acceleration.txid] = [];
for (const pool of acceleration.pools) {
if (!pools[pool]) {
pools[pool] = {
name: this.pools[pool]?.name || 'unknown',
block: 0,
vsize: 0,
accelerations: [],
complete: false,
};
}
pools[pool].accelerations.push(acceleration.txid);
}
for (const ancestor of mempoolCache[acceleration.txid].ancestors || []) {
accelerationPositions[ancestor.txid] = [];
}
}
for (const pool of Object.keys(pools)) {
// if any pools accepted *every* acceleration, we can just use the GBT result positions directly
if (pools[pool].accelerations.length === Object.keys(accelerations).length) {
pools[pool].complete = true;
}
}
let block = 0;
let index = 0;
let next = accQueue.pop();
// build simulated blocks for each pool by taking the best option from
// either the mempool or the list of accelerations.
while (next && block < mempoolBlocks.length) {
while (next && index < mempoolBlocks[block].transactions.length) {
const nextTx = mempoolBlocks[block].transactions[index];
if (next.rate >= (nextTx.rate || (nextTx.fee / nextTx.vsize))) {
for (const pool of next.acceleration.pools) {
if (pools[pool].vsize + next.vsize <= 999_000) {
pools[pool].vsize += next.vsize;
} else {
pools[pool].block++;
pools[pool].vsize = next.vsize;
}
// insert the acceleration into matching pool's blocks
if (pools[pool].complete && mempoolCache[next.acceleration.txid]?.position !== undefined) {
accelerationPositions[next.acceleration.txid].push({
...mempoolCache[next.acceleration.txid].position as { block: number, vsize: number },
poolId: pool,
pool: pools[pool].name
});
} else {
accelerationPositions[next.acceleration.txid].push({
poolId: pool,
pool: pools[pool].name,
block: pools[pool].block,
vsize: pools[pool].vsize - (next.vsize / 2),
});
}
// and any accelerated ancestors
for (const ancestor of mempoolCache[next.acceleration.txid].ancestors || []) {
if (pools[pool].complete && mempoolCache[ancestor.txid]?.position !== undefined) {
accelerationPositions[ancestor.txid].push({
...mempoolCache[ancestor.txid].position as { block: number, vsize: number },
poolId: pool,
pool: pools[pool].name,
});
} else {
accelerationPositions[ancestor.txid].push({
poolId: pool,
pool: pools[pool].name,
block: pools[pool].block,
vsize: pools[pool].vsize - (next.vsize / 2),
});
}
}
}
next = accQueue.pop();
} else {
// skip accelerated transactions and their CPFP ancestors
if (accelerationPositions[nextTx.txid] == null) {
// insert into all pools' blocks
for (const pool of Object.keys(pools)) {
if (pools[pool].vsize + nextTx.vsize <= 999_000) {
pools[pool].vsize += nextTx.vsize;
} else {
pools[pool].block++;
pools[pool].vsize = nextTx.vsize;
}
}
}
index++;
}
}
block++;
index = 0;
}
mempool.setAccelerationPositions(accelerationPositions);
}
}
export default new MempoolBlocks();

View File

@@ -27,6 +27,7 @@ class Mempool {
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
private accelerations: { [txId: string]: Acceleration } = {};
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
private txPerSecondArray: number[] = [];
private txPerSecond: number = 0;
@@ -514,6 +515,14 @@ class Mempool {
}
}
setAccelerationPositions(positions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] }): void {
this.accelerationPositions = positions;
}
getAccelerationPositions(txid: string): { [pool: number]: { poolId: number, pool: string, block: number, vsize: number } } | undefined {
return this.accelerationPositions[txid];
}
private startTimer() {
const state: any = {
start: Date.now(),

View File

@@ -10,6 +10,12 @@ export interface Acceleration {
effectiveFee: number,
feeDelta: number,
pools: number[],
positions?: {
[pool: number]: {
block: number,
vbytes: number,
},
},
};
export interface AccelerationHistory {

View File

@@ -206,7 +206,8 @@ class WebsocketHandler {
}
response['txPosition'] = JSON.stringify({
txid: trackTxid,
position
position,
accelerationPositions: memPool.getAccelerationPositions(tx.txid),
});
}
} else {
@@ -821,7 +822,8 @@ class WebsocketHandler {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
}
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
};
if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) {
calculateCpfp(mempoolTx, newMempool);
@@ -834,7 +836,7 @@ class WebsocketHandler {
effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null,
sigops: mempoolTx.sigops,
adjustedVsize: mempoolTx.adjustedVsize,
acceleration: mempoolTx.acceleration
acceleration: mempoolTx.acceleration,
};
}
response['txPosition'] = JSON.stringify(positionData);
@@ -1137,7 +1139,8 @@ class WebsocketHandler {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
acceleratedBy: mempoolTx.acceleratedBy || undefined,
}
},
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
});
}
}

View File

@@ -51,6 +51,7 @@ interface IConfig {
REQUEST_TIMEOUT: number;
FALLBACK_TIMEOUT: number;
FALLBACK: string[];
MAX_BEHIND_TIP: number;
};
LIGHTNING: {
ENABLED: boolean;
@@ -210,6 +211,7 @@ const defaults: IConfig = {
'REQUEST_TIMEOUT': 10000,
'FALLBACK_TIMEOUT': 5000,
'FALLBACK': [],
'MAX_BEHIND_TIP': 2,
},
'ELECTRUM': {
'HOST': '127.0.0.1',

View File

@@ -2,8 +2,7 @@ import * as fs from 'fs';
import path from 'path';
import config from './config';
import { createPool, Pool, PoolConnection } from 'mysql2/promise';
import { LogLevel } from './logger';
import logger from './logger';
import logger, { LogLevel } from './logger';
import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql';
import { execSync } from 'child_process';

View File

@@ -45,6 +45,7 @@ import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes';
import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes';
import mempoolBlocks from './api/mempool-blocks';
class Server {
private wss: WebSocket.Server | undefined;
@@ -149,6 +150,7 @@ class Server {
await poolsUpdater.updatePoolsJson(); // Needs to be done before loading the disk cache because we sometimes wipe it
await syncAssets.syncAssets$();
await mempoolBlocks.updatePools$();
if (config.MEMPOOL.ENABLED) {
if (config.MEMPOOL.CACHE_ENABLED) {
await diskCache.$loadMempoolCache();

View File

@@ -1,4 +1,4 @@
FROM node:20.13.1-buster-slim AS builder
FROM node:20.14.0-buster-slim AS builder
ARG commitHash
ENV MEMPOOL_COMMIT_HASH=${commitHash}
@@ -24,7 +24,7 @@ RUN npm install --omit=dev --omit=optional
WORKDIR /build
RUN npm run package
FROM node:20.13.1-buster-slim
FROM node:20.14.0-buster-slim
WORKDIR /backend

View File

@@ -60,7 +60,8 @@
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
"REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__,
"FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__,
"FALLBACK": __ESPLORA_FALLBACK__
"FALLBACK": __ESPLORA_FALLBACK__,
"MAX_BEHIND_TIP": __ESPLORA_MAX_BEHIND_TIP__
},
"SECOND_CORE_RPC": {
"HOST": "__SECOND_CORE_RPC_HOST__",

View File

@@ -62,6 +62,7 @@ __ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
__ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000}
__ESPLORA_FALLBACK__=${ESPLORA_FALLBACK:=[]}
__ESPLORA_MAX_BEHIND_TIP__=${ESPLORA_MAX_BEHIND_TIP:=2}
# SECOND_CORE_RPC
__SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1}
@@ -216,6 +217,7 @@ sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTE
sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK__!${__ESPLORA_FALLBACK__}!g" mempool-config.json
sed -i "s!__ESPLORA_MAX_BEHIND_TIP__!${__ESPLORA_MAX_BEHIND_TIP__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_HOST__!${__SECOND_CORE_RPC_HOST__}!g" mempool-config.json
sed -i "s!__SECOND_CORE_RPC_PORT__!${__SECOND_CORE_RPC_PORT__}!g" mempool-config.json

View File

@@ -1,4 +1,4 @@
FROM node:20.13.1-buster-slim AS builder
FROM node:20.14.0-buster-slim AS builder
ARG commitHash
ENV DOCKER_COMMIT_HASH=${commitHash}
@@ -13,7 +13,7 @@ RUN npm install --omit=dev --omit=optional
RUN npm run build
FROM nginx:1.26.0-alpine
FROM nginx:1.27.0-alpine
WORKDIR /patch

View File

@@ -32,6 +32,7 @@
"bootstrap": "~4.6.2",
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"cypress": "^13.12.0",
"domino": "^2.1.6",
"echarts": "~5.5.0",
"esbuild": "^0.21.1",
@@ -62,7 +63,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.10.0",
"cypress": "^13.12.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",
@@ -8028,9 +8029,9 @@
"peer": true
},
"node_modules/cypress": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.10.0.tgz",
"integrity": "sha512-tOhwRlurVOQbMduX+KonoMeQILs2cwR3yHGGENoFvvSoLUBHmJ8b9/n21gFSDqjlOJ+SRVcwuh+fG/JDsHsT6Q==",
"version": "13.12.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.12.0.tgz",
"integrity": "sha512-udzS2JilmI9ApO/UuqurEwOvThclin5ntz7K0BtnHBs+tg2Bl9QShLISXpSEMDv/u8b6mqdoAdyKeZiSqKWL8g==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
@@ -24111,9 +24112,9 @@
"peer": true
},
"cypress": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.10.0.tgz",
"integrity": "sha512-tOhwRlurVOQbMduX+KonoMeQILs2cwR3yHGGENoFvvSoLUBHmJ8b9/n21gFSDqjlOJ+SRVcwuh+fG/JDsHsT6Q==",
"version": "13.12.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.12.0.tgz",
"integrity": "sha512-udzS2JilmI9ApO/UuqurEwOvThclin5ntz7K0BtnHBs+tg2Bl9QShLISXpSEMDv/u8b6mqdoAdyKeZiSqKWL8g==",
"optional": true,
"requires": {
"@cypress/request": "^3.0.0",

View File

@@ -115,7 +115,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.10.0",
"cypress": "^13.12.0",
"cypress-fail-on-console-error": "~5.1.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",

View File

@@ -27,6 +27,7 @@ import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-st
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
import { AppPreloadingStrategy } from './app.preloading-strategy';
import { ServicesApiServices } from './services/services-api.service';
import { DatePipe } from '@angular/common';
const providers = [
ElectrsApiService,
@@ -45,6 +46,7 @@ const providers = [
FiatShortenerPipe,
FiatCurrencyPipe,
CapAddressPipe,
DatePipe,
AppPreloadingStrategy,
ServicesApiServices,
PreloadService,

View File

@@ -189,7 +189,7 @@
<ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</ng-container>
@@ -201,7 +201,7 @@
<div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>
@@ -214,7 +214,7 @@
<div class="wrapper">
<ng-container *ngIf="ogs$ | async as ogs; else loadingSponsors">
<a *ngFor="let ogSponsor of ogs" [href]="'https://twitter.com/' + ogSponsor.handle" target="_blank" rel="sponsored" [title]="ogSponsor.handle">
<img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-container>
</div>
@@ -355,7 +355,7 @@
<div class="wrapper">
<ng-template ngFor let-translator [ngForOf]="translators">
<a [href]="'https://twitter.com/' + translator.value" target="_blank" [title]="translator.key">
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>
@@ -369,7 +369,7 @@
<div class="wrapper">
<ng-template ngFor let-contributor [ngForOf]="contributors.regular">
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
<span>{{ contributor.name }}</span>
</a>
</ng-template>
@@ -381,7 +381,7 @@
<div class="wrapper">
<ng-template ngFor let-contributor [ngForOf]="contributors.core">
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name" [class]="'project-member-avatar'">
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/grumpy.svg'; this.className = 'image unknown'"/>
<span>{{ contributor.name }}</span>
</a>
</ng-template>

View File

@@ -10,9 +10,6 @@
margin: 25px;
line-height: 32px;
}
.unknown {
border: 1px solid #b4b4b4;
}
.image.not-rounded {
border-radius: 0;

View File

@@ -35,7 +35,7 @@
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
</td>
<td class="time text-right">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true"></app-time>
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
</td>
</ng-container>
<ng-container *ngIf="!pending">
@@ -55,7 +55,7 @@
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger" i18n="accelerator.canceled">Failed <span *ngIf="acceleration.status === 'failed_provisional'">🔄</span></span>
</td>
<td class="date text-right" *ngIf="!this.widget">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true"></app-time>
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
</td>
</ng-container>
</tr>

View File

@@ -14,7 +14,7 @@
</td>
<td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount></td>
<td class="table-cell-fiat" ><app-fiat [value]="transaction.value" [blockConversion]="transaction.price" digitsInfo="1.0-0"></app-fiat></td>
<td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true"></app-time></td>
<td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true" [showTooltip]="true"></app-time></td>
</tr>
</tbody>
<div class="">&nbsp;</div>

View File

@@ -66,7 +66,7 @@
</div>
<div class="row">
<div class="col-md">
<app-address-graph [address]="addressString" [isPubkey]="address?.is_pubkey" [stats]="address.chain_stats" [period]="balancePeriod" />
<app-address-graph [address]="addressString" [isPubkey]="address?.is_pubkey" [stats]="address.chain_stats" [period]="balancePeriod" left="80" />
</div>
</div>
</div>

View File

@@ -182,6 +182,8 @@ export class AddressComponent implements OnInit, OnDestroy {
if (!this.showBalancePeriod()) {
this.setBalancePeriod('all');
} else {
this.setBalancePeriod('1m');
}
},
(error) => {

View File

@@ -158,7 +158,7 @@
<tbody *ngIf="blocks$ | async as blocks; else blocksSkeleton">
<tr *ngFor="let block of blocks; let i = index; trackBy: trackByBlock">
<td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></td>
<td class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true" [showTooltip]="true"></app-time></td>
<td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
<td class="table-cell-size">
<div class="progress">

View File

@@ -14,7 +14,7 @@
<a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a>
</td>
<td class="date text-left">
<app-time kind="since" [time]="diffChange.timestamp" [fastRender]="true" [precision]="1"></app-time>
<app-time kind="since" [time]="diffChange.timestamp" [fastRender]="true" [precision]="1" [showTooltip]="true"></app-time>
</td>
<td class="text-right">{{ diffChange.difficultyShorten }}</td>
<td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">

View File

@@ -26,7 +26,7 @@
<app-amount [satoshis]="utxo.amount" [noFiat]="true" [forceBtc]="true"></app-amount>
</td>
<td class="timestamp text-left widget">
<app-time kind="since" [time]="utxo.blocktime"></app-time>
<app-time kind="since" [time]="utxo.blocktime" [showTooltip]="true"></app-time>
</td>
</tr>
</ng-container>

View File

@@ -31,7 +31,7 @@
</ng-container>
</td>
<td class="timestamp text-left widget">
<app-time kind="since" [time]="peg.blocktime"></app-time>
<app-time kind="since" [time]="peg.blocktime" [showTooltip]="true"></app-time>
</td>
<td class="amount text-right widget" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0, 'glow-effect': peg.amount < 0 && peg.bitcoinaddress && !peg.bitcointxid}">
<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>

View File

@@ -197,7 +197,7 @@
&lrm;{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm:ss' }}
</td>
<td class="mined">
<app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time>
<app-time kind="since" [time]="block.timestamp" [fastRender]="true" [showTooltip]="true"></app-time>
</td>
<td class="coinbase">
<span class="badge badge-secondary scriptmessage longer">

View File

@@ -23,7 +23,7 @@
<span *ngIf="tree.mined" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
<span *ngIf="tree.fullRbf" class="badge badge-info" i18n="transaction.full-rbf">Full RBF</span>
</span>
<app-time kind="since" [time]="tree.time"></app-time>
<app-time kind="since" [time]="tree.time" [showTooltip]="true"></app-time>
</p>
<div class="timeline-wrapper" [class.mined]="tree.mined">
<app-rbf-timeline [replacements]="tree"></app-rbf-timeline>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
<span [ngbTooltip]="tooltip">{{ text }}</span>

View File

@@ -1,15 +1,17 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
import { StateService } from '../../services/state.service';
import { dates } from '../../shared/i18n/dates';
import { DatePipe } from '@angular/common';
@Component({
selector: 'app-time',
template: `{{ text }}`,
templateUrl: './time.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TimeComponent implements OnInit, OnChanges, OnDestroy {
interval: number;
text: string;
tooltip: string;
precisionThresholds = {
year: 100,
month: 18,
@@ -26,6 +28,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
@Input() kind: 'plain' | 'since' | 'until' | 'span' | 'before' | 'within' = 'plain';
@Input() fastRender = false;
@Input() fixedRender = false;
@Input() showTooltip = false;
@Input() relative = false;
@Input() precision: number = 0;
@Input() numUnits: number = 1;
@@ -36,6 +39,7 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
constructor(
private ref: ChangeDetectorRef,
private stateService: StateService,
private datePipe: DatePipe,
) {
this.intervals = {
year: 31536000,
@@ -78,13 +82,20 @@ export class TimeComponent implements OnInit, OnChanges, OnDestroy {
switch (this.kind) {
case 'since':
seconds = Math.floor((+new Date() - +new Date(this.dateString || this.time * 1000)) / 1000);
this.tooltip = this.datePipe.transform(new Date(this.dateString || this.time * 1000), 'yyyy-MM-dd HH:mm');
break;
case 'until':
case 'within':
seconds = (+new Date(this.time) - +new Date()) / 1000;
this.tooltip = this.datePipe.transform(new Date(this.time), 'yyyy-MM-dd HH:mm');
break;
default:
seconds = Math.floor(this.time);
this.tooltip = '';
}
if (!this.showTooltip || this.relative) {
this.tooltip = '';
}
if (seconds < 1 && this.kind === 'span') {

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
export type TrackerStage = 'waiting' | 'pending' | 'soon' | 'next' | 'confirmed';
export type TrackerStage = 'waiting' | 'pending' | 'soon' | 'next' | 'confirmed' | 'replaced';
@Component({
selector: 'app-tracker-bar',

View File

@@ -43,16 +43,23 @@
<app-clockchain [height]="blockchainHeight" [width]="blockchainWidth" mode="none"></app-clockchain>
</div>
<div class="panel">
<div class="tracker-bar">
<app-tracker-bar [stage]="trackerStage"></app-tracker-bar>
</div>
@if (replaced) {
<div class="alert-replaced" role="alert">
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
<app-truncate [text]="latestReplacement" [lastChars]="12" [link]="['/tracker/' | relativeUrl, latestReplacement]"></app-truncate>
</div>
} @else {
<div class="tracker-bar">
<app-tracker-bar [stage]="trackerStage"></app-tracker-bar>
</div>
}
<div class="data">
@if (tx && !tx.status?.confirmed && mempoolPosition?.block != null) {
<div class="field narrower mt-2">
<div class="label" i18n="transaction.first-seen|Transaction first seen">First seen</div>
<div class="value">
@if (transactionTime > 0) {
<i><app-time kind="since" [time]="transactionTime" [fastRender]="true"></app-time></i>
<i><app-time kind="since" [time]="transactionTime" [fastRender]="true" [showTooltip]="true"></app-time></i>
} @else {
<span class="skeleton-loader" style="max-width: 50%;"></span>
}
@@ -79,7 +86,7 @@
<div class="value">
&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true"></app-time>)</i>
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true" [showTooltip]="true"></app-time>)</i>
</div>
</div>
</div>
@@ -143,6 +150,12 @@
</div>
<span class="explainer" i18n="tracker.explain.confirmed">Your transaction is confirmed!</span>
}
@case ('replaced') {
<div class="progress-icon">
<fa-icon [icon]="['fas', 'timeline']" [fixedWidth]="true"></fa-icon>
</div>
<span class="explainer" i18n="tracker.explain.replaced">Your transaction has been replaced by a newer version!</span>
}
}
}
}

View File

@@ -187,4 +187,12 @@
padding: 0.5em;
background: var(--primary);
color: white;
cursor: pointer;
}
.alert-replaced {
background: var(--danger);
margin: 1em 0.5em;
padding: 0.5em;
border-radius: 0.5em;
}

View File

@@ -76,6 +76,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
currencyChangeSubscription: Subscription;
rbfTransaction: undefined | Transaction;
replaced: boolean = false;
latestReplacement: string;
rbfReplaces: string[];
rbfInfo: RbfTree;
cpfpInfo: CpfpInfo | null;
@@ -218,6 +219,25 @@ export class TrackerComponent implements OnInit, OnDestroy {
).subscribe((rbfResponse) => {
this.rbfInfo = rbfResponse?.replacements;
this.rbfReplaces = rbfResponse?.replaces || null;
if (this.rbfInfo) {
// link to the latest pending version
this.latestReplacement = this.rbfInfo.tx.txid;
// or traverse the rbf tree to find a confirmed version
if (this.rbfInfo.mined) {
const stack = [this.rbfInfo];
let found = false;
while (stack.length && !found) {
const top = stack.pop();
if (top?.tx.mined) {
found = true;
this.latestReplacement = top.tx.txid;
break;
} else {
stack.push(...top.replaces);
}
}
}
}
});
this.fetchCachedTxSubscription = this.fetchCachedTx$
@@ -350,7 +370,9 @@ export class TrackerComponent implements OnInit, OnDestroy {
this.tx.acceleratedBy = txPosition.position?.acceleratedBy;
}
if (txPosition.position?.block === 0) {
if (this.replaced) {
this.trackerStage = 'replaced';
} else if (txPosition.position?.block === 0) {
this.trackerStage = 'next';
} else if (txPosition.position?.block < 3){
this.trackerStage = 'soon';
@@ -520,6 +542,10 @@ export class TrackerComponent implements OnInit, OnDestroy {
}
this.rbfTransaction = rbfTransaction;
this.replaced = true;
this.trackerStage = 'replaced';
if (!this.rbfInfo) {
this.latestReplacement = this.rbfTransaction.txid;
}
this.stateService.markBlock$.next({});
if (rbfTransaction && !this.tx) {
@@ -663,6 +689,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
this.isLoadingTx = true;
this.rbfTransaction = undefined;
this.replaced = false;
this.latestReplacement = '';
this.transactionTime = -1;
this.cpfpInfo = null;
this.adjustedVsize = null;

View File

@@ -450,7 +450,7 @@
@if (transactionTime > 0) {
<tr>
<td i18n="transaction.confirmed|Transaction Confirmed state">Confirmed</td>
<td><app-time kind="span" [time]="tx.status.block_time - transactionTime" [fastRender]="true" [relative]="true"></app-time></td>
<td><app-time kind="span" [time]="tx.status.block_time" [fastRender]="true" [showTooltip]="true"></app-time></td>
</tr>
}
} @else {
@@ -464,7 +464,7 @@
} @else if (transactionTime > 0) {
<tr>
<td i18n="transaction.first-seen|Transaction first seen">First seen</td>
<td><i><app-time kind="since" [time]="transactionTime" [fastRender]="true"></app-time></i></td>
<td><i><app-time kind="since" [time]="transactionTime" [fastRender]="true" [showTooltip]="true"></app-time></i></td>
</tr>
} @else {
<tr>

View File

@@ -13,7 +13,7 @@ import {
retry
} from 'rxjs/operators';
import { Transaction } from '../../interfaces/electrs.interface';
import { of, merge, Subscription, Observable, Subject, from, throwError, combineLatest } from 'rxjs';
import { of, merge, Subscription, Observable, Subject, from, throwError } from 'rxjs';
import { StateService } from '../../services/state.service';
import { CacheService } from '../../services/cache.service';
import { WebsocketService } from '../../services/websocket.service';
@@ -23,11 +23,11 @@ import { SeoService } from '../../services/seo.service';
import { StorageService } from '../../services/storage.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { getTransactionFlags } from '../../shared/transaction.utils';
import { Filter, toFilters, TransactionFlags } from '../../shared/filters.utils';
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration } from '../../interfaces/node-api.interface';
import { Filter, toFilters } from '../../shared/filters.utils';
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration, AccelerationPosition } from '../../interfaces/node-api.interface';
import { LiquidUnblinding } from './liquid-ublinding';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Price, PriceService } from '../../services/price.service';
import { PriceService } from '../../services/price.service';
import { isFeatureActive } from '../../bitcoin.utils';
import { ServicesApiServices } from '../../services/services-api.service';
import { EnterpriseService } from '../../services/enterprise.service';
@@ -62,6 +62,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
txId: string;
txInBlockIndex: number;
mempoolPosition: MempoolPosition;
accelerationPositions: AccelerationPosition[];
isLoadingTx = true;
error: any = undefined;
errorUnblinded: any = undefined;
@@ -372,10 +373,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.now = Date.now();
if (txPosition && txPosition.txid === this.txId && txPosition.position) {
this.mempoolPosition = txPosition.position;
this.accelerationPositions = txPosition.accelerationPositions;
if (this.tx && !this.tx.status.confirmed) {
const txFeePerVSize = this.getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated);
this.stateService.markBlock$.next({
txid: txPosition.txid,
mempoolPosition: this.mempoolPosition
txFeePerVSize,
mempoolPosition: this.mempoolPosition,
accelerationPositions: this.accelerationPositions,
});
this.txInBlockIndex = this.mempoolPosition.block;
@@ -390,6 +395,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
} else {
this.mempoolPosition = null;
this.accelerationPositions = null;
}
});
@@ -513,11 +519,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
});
this.fetchCpfp$.next(this.tx.txid);
} else {
const txFeePerVSize = this.getUnacceleratedFeeRate(this.tx, this.tx.acceleration || this.mempoolPosition?.accelerated);
if (tx.cpfpChecked) {
this.stateService.markBlock$.next({
txid: tx.txid,
txFeePerVSize: tx.effectiveFeePerVsize,
txFeePerVSize,
mempoolPosition: this.mempoolPosition,
accelerationPositions: this.accelerationPositions,
});
this.setCpfpInfo({
ancestors: tx.ancestors,
@@ -718,7 +726,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
setIsAccelerated(initialState: boolean = false) {
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool == this.pool.id || pool?.['pool_unique_id'] == this.pool.id))));
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id || pool?.['pool_unique_id'] === this.pool.id))));
if (this.isAcceleration && initialState) {
this.showAccelerationSummary = false;
}
@@ -791,6 +799,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.mempoolPosition = null;
this.pool = null;
this.auditStatus = null;
this.accelerationPositions = null;
document.body.scrollTo(0, 0);
this.isAcceleration = false;
this.leaveTransaction();

View File

@@ -8,7 +8,7 @@
<div>
<ng-template [ngIf]="tx.status.confirmed">&lrm;{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-template>
<ng-template [ngIf]="!tx.status.confirmed && tx.firstSeen">
<i><app-time kind="since" [time]="tx.firstSeen" [fastRender]="true"></app-time></i>
<i><app-time kind="since" [time]="tx.firstSeen" [fastRender]="true" [showTooltip]="true"></app-time></i>
</ng-template>
</div>
</div>
@@ -114,22 +114,33 @@
<td i18n="transactions-list.witness">Witness</td>
<td style="text-align: left;">
<ng-container *ngFor="let witness of vin.witness; index as i">
<input type="checkbox" [id]="'tx' + vindex + 'witness' + i" style="display: none;">
<p class="witness-item" [class.accordioned]="witness.length > 1000">
<ng-template [ngIf]="witness" [ngIfElse]="emptyWitnessItem">
<p class="witness-item">
@if (witness.length > 1000) {
@if (!showFullWitness[vindex][i]) {
{{ witness | slice:0:1000 }}
} @else {
{{ witness }}
}
} @else if (witness) {
{{ witness }}
</ng-template>
<ng-template #emptyWitnessItem>
} @else {
&lt;empty&gt;
</ng-template>
}
</p>
<div class="witness-toggle" *ngIf="witness.length > 1000">
<span class="ellipsis">...</span>
<label [for]="'tx' + vindex + 'witness' + i" class="btn btn-sm btn-primary mt-2">
<span class="show-all" i18n="show-all">Show all</span>
<span class="show-less" i18n="show-less">Show less</span>
</label>
</div>
@if (witness.length > 1000) {
<div style="display: flex;">
@if (!showFullWitness[vindex][i]) {
<span>...</span>
}
<label class="btn btn-sm btn-primary mt-2" (click)="toggleShowFullWitness(vindex, i)" style="margin-left: auto;">
@if (!showFullWitness[vindex][i]) {
<span i18n="show-all">Show all</span>
} @else {
<span i18n="show-less">Show less</span>
}
</label>
</div>
}
</ng-container>
</td>
</tr>
@@ -142,7 +153,16 @@
<ng-template #p2wsh>
<td i18n="transactions-list.p2wsh-witness-script">P2WSH witness script</td>
</ng-template>
<td style="text-align: left;" [innerHTML]="vin.inner_witnessscript_asm | asmStyler"></td>
<td style="text-align: left;">
<div [innerHTML]="vin.inner_witnessscript_asm | asmStyler:showFullScript[vindex]"></div>
<div *ngIf="vin.inner_witnessscript_asm.length > 1000" style="display: flex;">
<span *ngIf="!showFullScript[vindex]">...</span>
<label class="btn btn-sm btn-primary mt-2" (click)="toggleShowFullScript(vindex)" style="margin-left: auto;">
<span *ngIf="!showFullScript[vindex]" i18n="show-all">Show all</span>
<span *ngIf="showFullScript[vindex]" i18n="show-less">Show less</span>
</label>
</div>
</td>
</tr>
<tr>
<td i18n="transactions-list.nsequence">nSequence</td>

View File

@@ -172,42 +172,7 @@ h2 {
}
.vin-witness {
.witness-item.accordioned {
max-height: 300px;
overflow: hidden;
}
input:checked + .witness-item.accordioned {
max-height: none;
}
.witness-toggle {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 1em;
.show-all {
display: inline;
}
.show-less {
display: none;
}
.ellipsis {
visibility: visible;
}
}
input:checked ~ .witness-toggle {
.show-all {
display: none;
}
.show-less {
display: inline;
}
.ellipsis {
visibility: hidden;
}
.witness-item {
overflow: hidden;
}
}

View File

@@ -48,6 +48,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
transactionsLength: number = 0;
inputRowLimit: number = 12;
outputRowLimit: number = 12;
showFullScript: { [vinIndex: number]: boolean } = {};
showFullWitness: { [vinIndex: number]: { [witnessIndex: number]: boolean } } = {};
constructor(
public stateService: StateService,
@@ -300,7 +302,17 @@ export class TransactionsListComponent implements OnInit, OnChanges {
toggleDetails(): void {
if (this.showDetails$.value === true) {
this.showDetails$.next(false);
this.showFullScript = {};
this.showFullWitness = {};
} else {
this.showFullScript = this.transactions[0] ? this.transactions[0].vin.reduce((acc, _, i) => ({...acc, [i]: false}), {}) : {};
this.showFullWitness = this.transactions[0] ? this.transactions[0].vin.reduce((acc, vin, vinIndex) => {
acc[vinIndex] = vin.witness ? vin.witness.reduce((witnessAcc, _, witnessIndex) => {
witnessAcc[witnessIndex] = false;
return witnessAcc;
}, {}) : {};
return acc;
}, {}) : {};
this.showDetails$.next(true);
}
}
@@ -352,6 +364,14 @@ export class TransactionsListComponent implements OnInit, OnChanges {
return limit;
}
toggleShowFullScript(vinIndex: number): void {
this.showFullScript[vinIndex] = !this.showFullScript[vinIndex];
}
toggleShowFullWitness(vinIndex: number, witnessIndex: number): void {
this.showFullWitness[vinIndex][witnessIndex] = !this.showFullWitness[vinIndex][witnessIndex];
}
ngOnDestroy(): void {
this.outspendsSubscription.unsubscribe();
this.currencyChangeSubscription?.unsubscribe();

View File

@@ -110,7 +110,7 @@
<tbody *ngIf="blocks$ | async as blocks; else blocksSkeleton">
<tr *ngFor="let block of blocks; let i = index; trackBy: trackByBlock">
<td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></td>
<td class="table-cell-mined" ><app-time kind="since" [time]="block.timestamp" [fastRender]="true" [showTooltip]="true"></app-time></td>
<td class="table-cell-transaction-count">{{ block.tx_count | number }}</td>
<td class="table-cell-size">
<div class="progress">

View File

@@ -251,6 +251,11 @@ export interface MempoolPosition {
acceleratedBy?: number[],
}
export interface AccelerationPosition extends MempoolPosition {
pool: string;
offset?: number;
}
export interface RewardStats {
startBlock: number;
endBlock: number;

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface';
import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '../interfaces/websocket.interface';
import { Acceleration, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface';
import { Acceleration, AccelerationPosition, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
import { filter, map, scan, shareReplay } from 'rxjs/operators';
@@ -16,6 +16,7 @@ export interface MarkBlockState {
mempoolBlockIndex?: number;
txFeePerVSize?: number;
mempoolPosition?: MempoolPosition;
accelerationPositions?: AccelerationPosition[];
}
export interface ILoadingIndicators { [name: string]: number; }
@@ -145,7 +146,7 @@ export class StateService {
utxoSpent$ = new Subject<object>();
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
mempoolTransactions$ = new Subject<Transaction>();
mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null}>();
mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition, cpfp: CpfpInfo | null, accelerationPositions?: AccelerationPosition[] }>();
mempoolRemovedTransactions$ = new Subject<Transaction>();
multiAddressTransactions$ = new Subject<{ [address: string]: { mempool: Transaction[], confirmed: Transaction[], removed: Transaction[] }}>();
blockTransactions$ = new Subject<Transaction>();

View File

@@ -72,6 +72,7 @@
<p><a [routerLink]="['/docs/faq']" fragment="what-is-a-mempool-explorer" i18n="faq.what-is-a-mempool-exlorer">What is a mempool explorer?</a></p>
<p><a [routerLink]="['/docs/faq']" fragment="why-is-transaction-stuck-in-mempool" i18n="faq.why-isnt-my-transaction-confirming">Why isn't my transaction confirming?</a></p>
<p><a [routerLink]="['/docs/faq' | relativeUrl]" i18n="faq.more-faq">More FAQs &raquo;</a></p>
<p *ngIf="mempoolSpaceBuild"><a [routerLink]="['/research' | relativeUrl]" i18n="mempool-research">Research</a></p>
</div>
<div class="links" *ngIf="officialMempoolSpace || env.TESTNET_ENABLED || env.TESTNET4_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED else toolBox" >

View File

@@ -20,6 +20,7 @@ export class GlobalFooterComponent implements OnInit, OnDestroy {
private destroy$: Subject<any> = new Subject<any>();
env: Env;
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
mempoolSpaceBuild = window['isMempoolSpaceBuild'];
backendInfo$: Observable<IBackendInfo>;
servicesBackendInfo$: Observable<IBackendInfo>;
frontendGitCommitHash = this.stateService.env.GIT_COMMIT_HASH;

View File

@@ -5,13 +5,18 @@ import { Pipe, PipeTransform } from '@angular/core';
})
export class AsmStylerPipe implements PipeTransform {
transform(asm: string): string {
transform(asm: string, showAll = true): string {
const instructions = asm.split('OP_');
let out = '';
let chars = -3;
for (const instruction of instructions) {
if (instruction === '') {
continue;
}
if (!showAll && chars > 1000) {
break;
}
chars += instruction.length + 3;
out += this.addStyling(instruction);
}
return out;

View File

@@ -4,7 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
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,
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 } from '@fortawesome/free-solid-svg-icons';
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 } 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';
@@ -438,5 +438,6 @@ export class SharedModule {
library.addIcons(faHourglassEnd);
library.addIcons(faWandMagicSparkles);
library.addIcons(faFaucetDrip);
library.addIcons(faTimeline);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 323 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@
"express": "^4.19.2",
"ejs": "^3.1.10",
"node-fetch-commonjs": "^3.3.1",
"puppeteer": "^15.3.2",
"puppeteer": "^22.11.2",
"puppeteer-cluster": "^0.23.0",
"typescript": "~4.7.4"
},

View File

@@ -233,6 +233,10 @@ const routes = {
title: "Trademark Policy",
fallbackImg: '/resources/previews/trademark-policy.jpg',
},
research: {
title: "Mempool Research",
fallbackImg: '/resources/previews/research.jpg',
},
};
export const networks = {