Compare commits
40 Commits
mononaut/f
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bf21c2fb9 | ||
|
|
0c8d9daaec | ||
|
|
307d3627a0 | ||
|
|
db04c4663e | ||
|
|
a0a6a0da4f | ||
|
|
ccd8412e6a | ||
|
|
84dae82e90 | ||
|
|
684ad9f0e6 | ||
|
|
24b5d4e971 | ||
|
|
fda40cad48 | ||
|
|
2ce4b5604e | ||
|
|
fb660e8477 | ||
|
|
621def712d | ||
|
|
8382a27a7c | ||
|
|
b7d96a2a26 | ||
|
|
3149199c8a | ||
|
|
0c3ef4eabc | ||
|
|
fffcb5038f | ||
|
|
77d42bfdbb | ||
|
|
f840ac951b | ||
|
|
22a48efd19 | ||
|
|
fba3f7ec1c | ||
|
|
6b3005c49d | ||
|
|
17132ff047 | ||
|
|
355fe58b43 | ||
|
|
604b0ba3e6 | ||
|
|
d016838356 | ||
|
|
f77582250f | ||
|
|
976e505445 | ||
|
|
42c60fd991 | ||
|
|
9a838c7269 | ||
|
|
f31b28251c | ||
|
|
ced1595d70 | ||
|
|
0b0109d821 | ||
|
|
992da1e5d2 | ||
|
|
9d4bbe9317 | ||
|
|
5575798cb6 | ||
|
|
57cc53b64e | ||
|
|
37725bb341 | ||
|
|
568084e143 |
@@ -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",
|
||||
|
||||
14
backend/package-lock.json
generated
14
backend/package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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__",
|
||||
|
||||
@@ -63,6 +63,7 @@ describe('Mempool Backend Config', () => {
|
||||
REQUEST_TIMEOUT: 10000,
|
||||
FALLBACK_TIMEOUT: 5000,
|
||||
FALLBACK: [],
|
||||
MAX_BEHIND_TIP: 2,
|
||||
});
|
||||
|
||||
expect(config.CORE_RPC).toStrictEqual({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -10,6 +10,12 @@ export interface Acceleration {
|
||||
effectiveFee: number,
|
||||
feeDelta: number,
|
||||
pools: number[],
|
||||
positions?: {
|
||||
[pool: number]: {
|
||||
block: number,
|
||||
vbytes: number,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export interface AccelerationHistory {
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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__",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
15
frontend/package-lock.json
generated
15
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
margin: 25px;
|
||||
line-height: 32px;
|
||||
}
|
||||
.unknown {
|
||||
border: 1px solid #b4b4b4;
|
||||
}
|
||||
|
||||
.image.not-rounded {
|
||||
border-radius: 0;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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=""> </div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -182,6 +182,8 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (!this.showBalancePeriod()) {
|
||||
this.setBalancePeriod('all');
|
||||
} else {
|
||||
this.setBalancePeriod('1m');
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -197,7 +197,7 @@
|
||||
‎{{ 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">
|
||||
|
||||
@@ -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
1
frontend/src/app/components/time/time.component.html
Normal file
1
frontend/src/app/components/time/time.component.html
Normal file
@@ -0,0 +1 @@
|
||||
<span [ngbTooltip]="tooltip">{{ text }}</span>
|
||||
@@ -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') {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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">
|
||||
‎{{ 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>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div>
|
||||
<ng-template [ngIf]="tx.status.confirmed">‎{{ 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 {
|
||||
<empty>
|
||||
</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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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 »</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" >
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
BIN
frontend/src/resources/previews/research.jpg
Normal file
BIN
frontend/src/resources/previews/research.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 168 KiB |
BIN
frontend/src/resources/previews/research.png
Normal file
BIN
frontend/src/resources/previews/research.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
22
frontend/src/resources/profile/grumpy.svg
Normal file
22
frontend/src/resources/profile/grumpy.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 323 KiB |
1815
unfurler/package-lock.json
generated
1815
unfurler/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user