Compare commits

..
Author SHA1 Message Date
Mononaut 6bb9ffd21a Extend esplora api to support returning origin, improve stats pause logic 2023-09-14 22:44:24 +00:00
Mononaut 9a8e5b7896 Avoid logging statistics while affected by esplora failover 2023-09-14 18:33:11 +00:00
softsimon 83c285e17d Merge pull request #4199 from mempool/mononaut/fix-diff-skeleton-mouseover
Fix js error on mouseover on difficulty skeleton
2023-09-13 13:26:10 +04:00
softsimon 5fabf892ba Merge branch 'master' into mononaut/fix-diff-skeleton-mouseover 2023-09-13 13:22:19 +04:00
wiz 827b0f6ad1 ops: Make install script wait longer for mysql to start 2023-08-31 16:52:56 +09:00
wiz e8d85613b4 Merge pull request #4224 from mempool/mononaut/failover-timeout
Bump failover timeout to 5s
2023-08-31 02:38:10 +09:00
wiz ce3b599bab ops: Add /api/v1/services route for new backend 2023-08-31 02:36:22 +09:00
Mononaut 47a7564cfc Bump failover timeout to 5s 2023-08-31 02:25:28 +09:00
wiz 7744146ef7 Merge pull request #4188 from mempool/nymkappa/menu
User menu + integrated accelerator if available
2023-08-31 01:55:52 +09:00
wiz 9cb72cff31 Set apple-mobile-web-app-capable to yes 2023-08-31 01:35:10 +09:00
nymkappa 98119b3e54 Merge remote-tracking branch 'origin/nymkappa/menu' into nymkappa/menu 2023-08-30 18:32:56 +02:00
nymkappa f5a3e78cbe [css] fix spacing at the top x2 2023-08-30 18:32:14 +02:00
wiz 515d2656e7 Set apple-mobile-web-app-status-bar-style to black 2023-08-31 01:23:07 +09:00
wiz 339a21caaa Merge branch 'master' into nymkappa/menu 2023-08-31 01:01:00 +09:00
wiz a7c0d33de8 Merge pull request #4219 from mempool/mononaut/accelerator-preview-concept
Accelerator preview concept
2023-08-31 00:58:24 +09:00
Mononaut d1ef1afc9c Fix default min/max/default userbid 2023-08-31 00:32:54 +09:00
wiz bf4a1fcd7a Merge pull request #4221 from hunicus/add-meta-descriptions
Add meta descriptions
2023-08-31 00:29:46 +09:00
Mononaut d6044331e1 Hide balance unless insufficient for max cost 2023-08-31 00:15:09 +09:00
wiz 9fdb164b64 Merge branch 'master' into add-meta-descriptions 2023-08-31 00:09:57 +09:00
hunicus d691bf2714 Add meta titles+descriptions to pages missing them 2023-08-30 23:59:51 +09:00
hunicus cd366177ba Add meta descriptions for mempool and liquid 2023-08-30 21:10:59 +09:00
hunicus 6312884234 Add meta descriptions for bisq 2023-08-30 20:47:57 +09:00
hunicus d1f26f0491 Add meta descriptions to docs page
Also move all meta tag setting to ngDoCheck()
because page titles were not updating when
user switched docs tabs.
2023-08-30 20:47:57 +09:00
Mononaut ed12e30517 Hide fee diagram on mobile 2023-08-30 17:16:39 +09:00
Mononaut cb363aca23 Simplify acceleration quote 2023-08-30 16:55:24 +09:00
Mononaut c753a8e92a Accelerator fee diagram concept 2023-08-30 16:55:24 +09:00
nymkappa c44276027c [auto scroll] fix documention anchor scrolling 2023-08-29 15:51:17 +02:00
nymkappa 91fbd0864b [css] fix footer position 2023-08-29 14:34:56 +02:00
nymkappa c2c4047ffd [css] fix spacing at the top 2023-08-29 14:19:02 +02:00
nymkappa 9427ba96a2 Merge branch 'master' into nymkappa/menu 2023-08-29 10:53:09 +02:00
nymkappa 7c11f0a3da [tx] fix eta css 2023-08-29 10:50:13 +02:00
softsimon 6780ba82d0 Merge pull request #4213 from mempool/mononaut/live-cpfp-updates
Send cpfp/effective fee rate changes to subscribed websocket clients
2023-08-29 10:17:33 +02:00
softsimon 6d643604d0 Merge branch 'master' into mononaut/live-cpfp-updates 2023-08-29 10:12:55 +02:00
nymkappa 7ae4b451e4 [auth] remove auto logout when imageMd5 no in localstorage 2023-08-29 09:31:18 +02:00
nymkappa bbd1f088d1 [footer] only show cta if official && ACCELERATOR 2023-08-28 16:20:30 +02:00
nymkappa 3060aecddb [status badge] reposition a bit higher 2023-08-28 15:57:06 +02:00
nymkappa 7a765cecd9 [footer] only show cta if official 2023-08-28 15:41:45 +02:00
nymkappa 03e9592c79 Merge pull request #4216 from mempool/mononaut/fix-menu-blockchain-offset
Fix bad blockchain offset after services -> dash
2023-08-28 10:56:18 +02:00
Mononaut c7b89f31dd Fix bad blockchain offset after services -> dash 2023-08-28 17:42:32 +09:00
nymkappa 899b230760 Merge remote-tracking branch 'origin/nymkappa/menu' into nymkappa/menu 2023-08-28 10:24:52 +02:00
nymkappa d787ef99c4 [typo] the Mempool Accelerator -> Mempool Accelerator 2023-08-28 10:24:44 +02:00
wiz 043ab008db Merge branch 'master' into nymkappa/menu 2023-08-28 16:43:03 +09:00
nymkappa 4abc4e96a8 [typo] satss -> sats 2023-08-28 09:34:50 +02:00
nymkappa 7fd8790750 [auth] fix blinking profile picture 2023-08-28 09:29:13 +02:00
nymkappa 8ba4a7b421 [ui] polish x2 2023-08-27 19:17:03 +02:00
nymkappa 78ea9cbd16 [ui] polish x1 2023-08-27 12:52:58 +02:00
softsimon 727937b8ae Merge pull request #4135 from mempool/hunicus/citadel-link
Change citadel link on about page
2023-08-27 11:42:52 +02:00
nymkappa 5f4add3e22 [tx] fix css when accel not available 2023-08-27 09:07:47 +02:00
wiz c4f8afbaf7 Merge pull request #4195 from mempool/junderw/rusttoolchain 2023-08-27 13:19:15 +09:00
Mononaut 528877f43f Send cpfp/effective fee rate changes to subscribed ws clients 2023-08-27 00:30:55 +09:00
nymkappa a3d61fa525 [accelerator] show payment preview when not logged in 2023-08-26 15:07:05 +02:00
nymkappa c0308fbc0d Merge branch 'master' into nymkappa/menu 2023-08-26 14:01:07 +02:00
softsimon 24696d0408 Merge pull request #4183 from mempool/mononaut/attitude-adjustment
Don't overload core with mempool tx requests
2023-08-26 12:28:26 +02:00
nymkappa b9838fda8d Merge branch 'master' into mononaut/attitude-adjustment 2023-08-26 11:48:21 +02:00
nymkappa 726bd51abb [debug] update versioning print 2023-08-26 11:27:45 +02:00
nymkappa 1fe08d1234 [accelerator] fix overflow in tx page integrated accel 2023-08-26 10:05:04 +02:00
nymkappa 1ca136fa75 Merge pull request #4212 from mempool/nymkappa/accelerate-preview
Nymkappa/accelerate preview
2023-08-26 09:53:29 +02:00
nymkappa fcecbe4967 [tx] integrated accelerator 2023-08-26 09:52:55 +02:00
hunicus d42a3f74ec Add description to index html and seo service 2023-08-26 14:18:55 +09:00
wiz dc44f1b618 ops: Fix WebGL for unfurler 2023-08-26 04:40:12 +09:00
wiz 0fde6dd908 ops: Bump prod NodeJS version to v20.5.1 2023-08-25 23:34:50 +09:00
wiz 4eb494baec ops: Increase FreeBSD bitcoin node shutdown timeout to 600 2023-08-25 23:29:03 +09:00
wiz c1b2f1f2c7 ops: Disable tor in prod install script 2023-08-25 23:24:51 +09:00
wiz a6d2887847 Merge branch 'master' into nymkappa/menu 2023-08-25 23:13:32 +09:00
nymkappa f9895a492c Merge branch 'nymkappa/menu' into nymkappa/accelerate-preview 2023-08-24 18:23:30 +02:00
nymkappa ac56f70f6f Merge pull request #4176 from mempool/nymkappa/fix-eta
[tx] fix eta css with accelerate button
2023-08-24 18:21:27 +02:00
nymkappa c89e283c8e [tx] start adding acceleration preview 2023-08-24 14:17:31 +02:00
nymkappa 3aa938a94b Merge pull request #4202 from mempool/mononaut/dynamic-width-chain
Dynamic width chain
2023-08-24 10:42:46 +02:00
Mononaut 20fff97804 restore window resize listener 2023-08-24 17:28:21 +09:00
Mononaut d1f7026804 hambuger 2023-08-24 17:28:19 +09:00
Mononaut 2da31c4d4a Slide blockchain to compensate for menu 2023-08-24 17:27:31 +09:00
Mononaut 975ba653a2 Dynamically resize blockchain to fit container 2023-08-24 17:27:31 +09:00
nymkappa c8100712e8 [footer] polish breakpoints in /services/* 2023-08-23 16:17:07 +02:00
nymkappa a7b2c39f51 [footer] re-add missing tagline 2023-08-23 15:02:36 +02:00
nymkappa 3d9fa5076b Merge remote-tracking branch 'origin/nymkappa/menu' into nymkappa/menu 2023-08-23 14:40:29 +02:00
nymkappa 9fa715e403 [footer] re-add main CTA with better design 2023-08-23 14:40:16 +02:00
nymkappa 9ee11b64e9 [menu] disable hamburger icon when not logged in 2023-08-23 14:39:58 +02:00
nymkappa 8c90d4ba98 Merge branch 'master' into nymkappa/menu 2023-08-23 12:21:23 +02:00
nymkappa b333c211f7 [menu] fix non clickable hamburger when logged out 2023-08-23 11:50:47 +02:00
nymkappa 8fb566858f [css] make sure <main> cannot be larger than viewport 2023-08-22 15:44:32 +02:00
nymkappa 379e1470fd [menu] fix menu close trigger, fix menu scroll on mobile safari 2023-08-22 14:59:23 +02:00
nymkappa c5f0608b46 [debug] footer version same color, inline version in about page 2023-08-22 14:51:05 +02:00
nymkappa 7544357826 [ui] redesign UX for navbar using sticky 2023-08-22 08:35:10 +02:00
nymkappa 5005817529 [ui] tweak profile image 2023-08-21 23:18:55 +02:00
nymkappa bfc3fbe397 [ui] hide the navbar on scroll 2023-08-21 22:19:05 +02:00
nymkappa 30be7e0f7a [ui] redesign UX for navbar 2023-08-21 22:08:25 +02:00
junderw 18eb6600b0 Fix README to include workaround for legacy versions of npm 2023-08-21 12:53:20 -07:00
junderw 88c0d04c18 Match CI toolchain with rust-toolchain 2023-08-21 12:53:20 -07:00
Mononaut 8ea7bb907c Fix js error on mouseover on difficulty skeleton 2023-08-22 02:40:00 +09:00
junderw b595002c25 Revert "Add a missing [workspace] tag in rust-gbt Cargo to build in git"
This reverts commit fadc46f3b5.
2023-08-21 10:01:40 -07:00
softsimon 7674bee5ad Animating menu 2023-08-21 17:06:12 +02:00
nymkappa 0ee28a335f [header] show anon image if user did not set a picture (to improve later) 2023-08-21 17:04:17 +02:00
nymkappa 285485c69e [about page] /api/v1/about-page -> /api/v1/services/sponsors 2023-08-21 15:27:55 +02:00
nymkappa b8222bff43 [auth] fix auth not being cleared after logout 2023-08-21 15:01:31 +02:00
nymkappa 3321ae0f1f [debug] always show git hashes in footer 2023-08-21 11:36:40 +02:00
nymkappa 4010047344 Merge remote-tracking branch 'origin/nymkappa/menu' into nymkappa/menu 2023-08-21 11:20:20 +02:00
nymkappa ce290a449c [debug] show more git hashes in about and footer 2023-08-21 11:19:54 +02:00
Jonathan Underwood b9c151f549 Update rust-toolchain 2023-08-21 17:47:58 +09:00
nymkappa 2ff3e4acb9 Merge branch 'master' into nymkappa/menu 2023-08-21 09:14:24 +02:00
nymkappa a1e2d2fd74 [debug] SERVICES -> GIT_COMMIT_HASH_MEMPOOL_SPACE in env 2023-08-21 08:57:27 +02:00
nymkappa e82b43e340 [footer] fix footer sizing in /services 2023-08-20 22:54:12 +02:00
nymkappa 22886cb32d [debug] show services backend version in /services global footer, show services global frontend build in /about 2023-08-20 22:53:33 +02:00
nymkappa ef554ad67b Merge branch 'master' into nymkappa/menu 2023-08-20 08:21:38 +02:00
nymkappa b64b6fb3c7 [menu] fix json.parse on missing auth in localstorage 2023-08-20 08:11:55 +02:00
Mononaut 2819cea509 Reduce core mempool tx sync to 8 concurrent requests 2023-08-19 19:02:30 +09:00
junderw 2a8a403da7 Use p-limit to limit concurrent requests 2023-08-19 18:53:32 +09:00
Mononaut e4fcadf39b More verbose comments on $getMempoolTransactionsExtended 2023-08-19 04:47:19 +09:00
nymkappa c4f2f4ca66 [menu] handle logout without reload, show signin in sidebar when not logged in 2023-08-18 18:33:09 +02:00
nymkappa 0c1221dc07 [menu] only show on official mainnet with accelerator enabled 2023-08-18 18:12:45 +02:00
nymkappa d1c9e8b56e [menu] show username at the top of the menu 2023-08-18 18:04:40 +02:00
Mononaut 1b2122cd35 Don't overload core with mempool tx requests 2023-08-19 01:02:27 +09:00
nymkappa 5aff2c74e6 [menu] show hamburger when logged out, fix menu scrolling on small screen 2023-08-18 17:56:07 +02:00
nymkappa 1bdbb1b908 [menu] logout is a special item 2023-08-17 22:13:06 +02:00
nymkappa 91e3943a74 [menu] call services api to fetch user menu 2023-08-17 18:59:27 +02:00
nymkappa 23b871631a [menu] write menu component with hardcoded values 2023-08-17 18:51:39 +02:00
nymkappa ab911d5c9e [tx] fix eta css with accelerate button 2023-08-17 14:28:33 +02:00
hunicus 45ba50e82c Change citadel link on about page 2023-08-10 16:26:07 +09:00
133 changed files with 2332 additions and 486 deletions

No files matched your search

+11 -2
View File
@@ -27,8 +27,17 @@ jobs:
node-version: ${{ matrix.node }}
registry-url: "https://registry.npmjs.org"
- name: Install 1.63.x Rust toolchain
uses: dtolnay/rust-toolchain@1.63
- name: Read rust-toolchain file from repository
id: gettoolchain
run: echo "::set-output name=toolchain::$(cat rust-toolchain)"
working-directory: ${{ matrix.node }}/${{ matrix.flavor }}
- name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain
# Latest version available on this commit is 1.71.1
# Commit date is Aug 3, 2023
uses: dtolnay/rust-toolchain@f361669954a8ecfc00a3443f35f9ac8e610ffc06
with:
toolchain: ${{ steps.gettoolchain.outputs.toolchain }}
- name: Install
if: ${{ matrix.flavor == 'dev'}}
+1 -1
View File
@@ -85,7 +85,7 @@ Install dependencies with `npm` and build the backend:
```
cd backend
npm install
npm install --no-install-links # npm@9.4.2 and later can omit the --no-install-links
npm run build
```
-2
View File
@@ -6,8 +6,6 @@ authors = ["mononaut"]
edition = "2021"
publish = false
[workspace]
[lib]
crate-type = ["cdylib"]
@@ -1,7 +1,7 @@
import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi {
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
$getRawMempool(): Promise<{ txids: IEsploraApi.Transaction['txid'][], local: boolean}>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
$getAllMempoolTransactions(lastTxid: string);
@@ -25,6 +25,7 @@ export interface AbstractBitcoinApi {
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
startHealthChecks(): void;
isFailedOver(): boolean;
}
export interface BitcoinRpcCredentials {
host: string;
+9 -2
View File
@@ -137,8 +137,12 @@ class BitcoinApi implements AbstractBitcoinApi {
throw new Error('Method getScriptHashTransactions not supported by the Bitcoin RPC API.');
}
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
return this.bitcoindClient.getRawMemPool();
async $getRawMempool(): Promise<{ txids: IEsploraApi.Transaction['txid'][], local: boolean}> {
const txids = await this.bitcoindClient.getRawMemPool();
return {
txids,
local: true,
};
}
$getAddressPrefix(prefix: string): string[] {
@@ -356,6 +360,9 @@ class BitcoinApi implements AbstractBitcoinApi {
}
public startHealthChecks(): void {};
public isFailedOver(): boolean {
return false;
}
}
export default BitcoinApi;
+2 -2
View File
@@ -638,8 +638,8 @@ class BitcoinRoutes {
private async getMempoolTxIds(req: Request, res: Response) {
try {
const rawMempool = await bitcoinApi.$getRawMempool();
res.send(rawMempool);
const { txids } = await bitcoinApi.$getRawMempool();
res.send(txids);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
+36 -10
View File
@@ -4,6 +4,7 @@ import http from 'http';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger';
import mempool from '../mempool';
interface FailoverHost {
host: string,
@@ -17,6 +18,8 @@ interface FailoverHost {
}
class FailoverRouter {
isFailedOver: boolean = false;
preferredHost: FailoverHost;
activeHost: FailoverHost;
fallbackHost: FailoverHost;
hosts: FailoverHost[];
@@ -46,6 +49,7 @@ class FailoverRouter {
socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
preferred: true,
};
this.preferredHost = this.activeHost;
this.fallbackHost = this.activeHost;
this.hosts.unshift(this.activeHost);
this.multihost = this.hosts.length > 1;
@@ -75,9 +79,9 @@ class FailoverRouter {
const results = await Promise.allSettled(this.hosts.map(async (host) => {
if (host.socket) {
return this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: 2000 });
return this.pollConnection.get<number>('/blocks/tip/height', { socketPath: host.host, timeout: 5000 });
} else {
return this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: 2000 });
return this.pollConnection.get<number>(host.host + '/blocks/tip/height', { timeout: 5000 });
}
}));
const maxHeight = results.reduce((max, result) => Math.max(max, result.status === 'fulfilled' ? result.value?.data || 0 : 0), 0);
@@ -151,6 +155,7 @@ class FailoverRouter {
this.sortHosts();
this.activeHost = this.hosts[0];
logger.warn(`Switching esplora host to ${this.activeHost.host}`);
this.isFailedOver = this.activeHost !== this.preferredHost;
}
private addFailure(host: FailoverHost): FailoverHost {
@@ -164,7 +169,7 @@ class FailoverRouter {
}
}
private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> {
private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true, withSource = false): Promise<T | { data: T, host: FailoverHost }> {
let axiosConfig;
let url;
if (host.socket) {
@@ -177,8 +182,17 @@ class FailoverRouter {
return (method === 'post'
? this.requestConnection.post<T>(url, data, axiosConfig)
: this.requestConnection.get<T>(url, axiosConfig)
).then((response) => { host.failures = Math.max(0, host.failures - 1); return response.data; })
.catch((e) => {
).then((response) => {
host.failures = Math.max(0, host.failures - 1);
if (withSource) {
return {
data: response.data,
host,
};
} else {
return response.data;
}
}).catch((e) => {
let fallbackHost = this.fallbackHost;
if (e?.response?.status !== 404) {
logger.warn(`esplora request failed ${e?.response?.status || 500} ${host.host}${path}`);
@@ -186,7 +200,7 @@ class FailoverRouter {
}
if (retry && e?.code === 'ECONNREFUSED' && this.multihost) {
// Retry immediately
return this.$query(method, path, data, responseType, fallbackHost, false);
return this.$query(method, path, data, responseType, fallbackHost, false, withSource);
} else {
throw e;
}
@@ -194,19 +208,27 @@ class FailoverRouter {
}
public async $get<T>(path, responseType = 'json'): Promise<T> {
return this.$query<T>('get', path, null, responseType);
return this.$query<T>('get', path, null, responseType, this.activeHost, true) as Promise<T>;
}
public async $post<T>(path, data: any, responseType = 'json'): Promise<T> {
return this.$query<T>('post', path, data, responseType);
return this.$query<T>('post', path, data, responseType) as Promise<T>;
}
public async $getWithSource<T>(path, responseType = 'json'): Promise<{ data: T, host: FailoverHost }> {
return this.$query<T>('get', path, null, responseType, this.activeHost, true, true) as Promise<{ data: T, host: FailoverHost }>;
}
}
class ElectrsApi implements AbstractBitcoinApi {
private failoverRouter = new FailoverRouter();
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
return this.failoverRouter.$get<IEsploraApi.Transaction['txid'][]>('/mempool/txids');
async $getRawMempool(): Promise<{ txids: IEsploraApi.Transaction['txid'][], local: boolean}> {
const result = await this.failoverRouter.$getWithSource<IEsploraApi.Transaction['txid'][]>('/mempool/txids', 'json');
return {
txids: result.data,
local: result.host === this.failoverRouter.preferredHost,
};
}
$getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
@@ -302,6 +324,10 @@ class ElectrsApi implements AbstractBitcoinApi {
public startHealthChecks(): void {
this.failoverRouter.startHealthChecks();
}
public isFailedOver(): boolean {
return this.failoverRouter.isFailedOver;
}
}
export default ElectrsApi;
+46 -135
View File
@@ -29,10 +29,6 @@ import websocketHandler from './websocket-handler';
import redisCache from './redis-cache';
import rbfCache from './rbf-cache';
import { calcBitsDifference } from './difficulty-adjustment';
import os from 'os';
import { Worker } from 'worker_threads';
import path from 'path';
class Blocks {
private blocks: BlockExtended[] = [];
@@ -410,8 +406,6 @@ class Blocks {
return;
}
const workerPool: Worker[] = [];
try {
// Get all indexed block hash
const indexedBlocks = await blocksRepository.$getIndexedBlocks();
@@ -426,67 +420,39 @@ class Blocks {
let newlyIndexed = 0;
let totalIndexed = indexedBlockSummariesHashesArray.length;
let indexedThisRun = 0;
let timer = Date.now();
const startedAt = Date.now();
let timer = Date.now() / 1000;
const startedAt = Date.now() / 1000;
const blocksToIndex = indexedBlocks.filter(block => !indexedBlockSummariesHashes[block.hash]);
if (!blocksToIndex.length) {
return;
}
const numWorkers = Math.max(1, os.cpus().length - 1);
for (let i = 0; i < numWorkers; i++) {
workerPool.push(new Worker(path.resolve(__dirname, '../index-workers/block-summary-worker.js')));
}
const promises: Promise<void>[] = [];
// This function assigns a task to a worker
const assignTask = (worker: Worker): boolean => {
if (blocksToIndex.length === 0) {
return false;
} else {
worker.postMessage(blocksToIndex.shift());
return true;
for (const block of indexedBlocks) {
if (indexedBlockSummariesHashes[block.hash] === true) {
continue;
}
};
const handleResult = (height: number): void => {
// Logging
const elapsedSeconds = (Date.now() / 1000) - timer;
if (elapsedSeconds > 5) {
const runningFor = (Date.now() / 1000) - startedAt;
const blockPerSeconds = indexedThisRun / elapsedSeconds;
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor.toFixed(2)} seconds`, logger.tags.mining);
timer = Date.now() / 1000;
indexedThisRun = 0;
}
if (config.MEMPOOL.BACKEND === 'esplora') {
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
} else {
await this.$getStrippedBlockTransactions(block.hash, true, true); // This will index the block summary
}
// Logging
indexedThisRun++;
totalIndexed++;
newlyIndexed++;
const elapsed = Date.now() - timer;
if (elapsed > 5000) {
const runningFor = Date.now() - startedAt;
const blockPerSeconds = indexedThisRun / (elapsed / 1000);
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
logger.debug(`Indexing block summary for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${(runningFor / 1000).toFixed(2)} seconds`, logger.tags.mining);
timer = Date.now();
indexedThisRun = 0;
}
};
// Start a task on each worker
for (const worker of workerPool) {
promises.push(new Promise((resolve, reject) => {
worker.removeAllListeners();
worker.on('message', (result) => {
// Handle the result, then assign a new task to the worker
handleResult(result);
if (!assignTask(worker)) {
resolve();
};
});
worker.on('error', reject);
if (!assignTask(worker)) {
resolve();
}
}));
}
await Promise.all(promises);
if (newlyIndexed > 0) {
logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining);
} else {
@@ -495,14 +461,6 @@ class Blocks {
} catch (e) {
logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
throw e;
} finally {
for (const worker of workerPool) {
if (worker) {
// clean up the workers
worker.removeAllListeners();
worker.terminate();
}
}
}
}
@@ -599,7 +557,6 @@ class Blocks {
* [INDEXING] Index all blocks metadata for the mining dashboard
*/
public async $generateBlockDatabase(): Promise<boolean> {
const workerPool: Worker[] = [];
try {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
let currentBlockHeight = blockchainInfo.blocks;
@@ -618,18 +575,12 @@ class Blocks {
let totalIndexed = await blocksRepository.$blockCountBetweenHeight(currentBlockHeight, lastBlockToIndex);
let indexedThisRun = 0;
let newlyIndexed = 0;
const startedAt = Date.now();
let timer = Date.now();
if (currentBlockHeight >= lastBlockToIndex) {
const numWorkers = Math.max(1, os.cpus().length - 1);
for (let i = 0; i < numWorkers; i++) {
workerPool.push(new Worker(path.resolve(__dirname, '../index-workers/block-worker.js')));
}
}
const startedAt = Date.now() / 1000;
let timer = Date.now() / 1000;
while (currentBlockHeight >= lastBlockToIndex) {
const endBlock = Math.max(0, lastBlockToIndex, currentBlockHeight - chunkSize + 1);
const missingBlockHeights: number[] = await blocksRepository.$getMissingBlocksBetweenHeights(
currentBlockHeight, endBlock);
if (missingBlockHeights.length <= 0) {
@@ -639,65 +590,33 @@ class Blocks {
logger.info(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`, logger.tags.mining);
const promises: Promise<void>[] = [];
// This function assigns a task to a worker
const assignTask = (worker: Worker): boolean => {
if (missingBlockHeights.length === 0) {
return false;
} else {
worker.postMessage({ height: missingBlockHeights.shift() });
return true;
for (const blockHeight of missingBlockHeights) {
if (blockHeight < lastBlockToIndex) {
break;
}
};
const handleResult = (height: number): void => {
indexedThisRun++;
totalIndexed++;
newlyIndexed++;
const elapsed = Date.now() - timer;
if (elapsed > 5000 || height === lastBlockToIndex) {
const runningFor = Date.now() - startedAt;
const blockPerSeconds = indexedThisRun / (elapsed / 1000);
++indexedThisRun;
++totalIndexed;
const elapsedSeconds = (Date.now() / 1000) - timer;
if (elapsedSeconds > 5 || blockHeight === lastBlockToIndex) {
const runningFor = (Date.now() / 1000) - startedAt;
const blockPerSeconds = indexedThisRun / elapsedSeconds;
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
logger.debug(`Indexing block #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress.toFixed(2)}%) | elapsed: ${(runningFor / 1000).toFixed(2)} seconds`, logger.tags.mining);
timer = Date.now();
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress.toFixed(2)}%) | elapsed: ${runningFor.toFixed(2)} seconds`, logger.tags.mining);
timer = Date.now() / 1000;
indexedThisRun = 0;
loadingIndicators.setProgress('block-indexing', progress, false);
}
};
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true, null, true);
const blockExtended = await this.$getBlockExtended(block, transactions);
// Start a task on each worker
for (const worker of workerPool) {
promises.push(new Promise((resolve, reject) => {
worker.removeAllListeners();
worker.on('message', (result) => {
// Handle the result, then assign a new task to the worker
handleResult(result);
if (!assignTask(worker)) {
resolve();
};
});
worker.on('error', reject);
if (!assignTask(worker)) {
resolve();
}
}));
newlyIndexed++;
await blocksRepository.$saveBlockInDatabase(blockExtended);
}
await Promise.all(promises);
currentBlockHeight -= chunkSize;
}
for (const worker of workerPool) {
if (worker) {
// clean up the workers
worker.removeAllListeners();
worker.terminate();
}
}
if (newlyIndexed > 0) {
logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining);
} else {
@@ -708,14 +627,6 @@ class Blocks {
logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
loadingIndicators.setProgress('block-indexing', 100);
throw e;
} finally {
for (const worker of workerPool) {
if (worker) {
// clean up the workers
worker.removeAllListeners();
worker.terminate();
}
}
}
return await BlocksRepository.$validateChain();
+13
View File
@@ -451,6 +451,7 @@ class MempoolBlocks {
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
for (const [txid, rate] of rates) {
if (txid in mempool) {
mempool[txid].cpfpDirty = (rate !== mempool[txid].effectiveFeePerVsize);
mempool[txid].effectiveFeePerVsize = rate;
mempool[txid].cpfpChecked = false;
}
@@ -494,6 +495,9 @@ class MempoolBlocks {
}
}
});
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
mempoolTx.cpfpDirty = true;
}
Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
}
}
@@ -531,12 +535,21 @@ class MempoolBlocks {
const acceleration = accelerations[txid];
if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
if (!mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
mempoolTx.acceleration = true;
for (const ancestor of mempoolTx.ancestors || []) {
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
}
mempool[ancestor.txid].acceleration = true;
isAccelerated[ancestor.txid] = true;
}
} else {
if (mempoolTx.acceleration) {
mempoolTx.cpfpDirty = true;
}
delete mempoolTx.acceleration;
}
+24
View File
@@ -26,6 +26,9 @@ class Mempool {
private accelerations: { [txId: string]: Acceleration } = {};
private failoverTimes: number[] = [];
private statisticsPaused: boolean = false;
private txPerSecondArray: number[] = [];
private txPerSecond: number = 0;
@@ -164,6 +167,15 @@ class Mempool {
return this.mempoolInfo;
}
public getStatisticsIsPaused(): boolean {
return this.statisticsPaused;
}
public logFailover(): void {
this.failoverTimes.push(Date.now());
this.statisticsPaused = true;
}
public getTxPerSecond(): number {
return this.txPerSecond;
}
@@ -242,6 +254,10 @@ class Mempool {
logger.debug(`fetched ${txs.length} transactions`);
this.updateTimerProgress(timer, 'fetched new transactions');
if (bitcoinApi.isFailedOver()) {
this.failoverTimes.push(Date.now());
}
for (const transaction of txs) {
this.mempoolCache[transaction.txid] = transaction;
if (this.inSync) {
@@ -259,6 +275,10 @@ class Mempool {
}
}
if (bitcoinApi.isFailedOver()) {
this.failoverTimes.push(Date.now());
}
if (txs.length < slice.length) {
const missing = slice.length - txs.length;
if (config.MEMPOOL.BACKEND === 'esplora') {
@@ -491,6 +511,10 @@ class Mempool {
private updateTxPerSecond() {
const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
this.failoverTimes = this.failoverTimes.filter((unixTime) => unixTime > nowMinusTimeSpan);
this.statisticsPaused = this.failoverTimes.length > 0;
this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan);
this.txPerSecond = this.txPerSecondArray.length / config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD || 0;
+1 -1
View File
@@ -53,7 +53,7 @@ class RbfCache {
private expiring: Map<string, number> = new Map();
private cacheQueue: CacheEvent[] = [];
public init(): void {
constructor() {
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
}
+16 -13
View File
@@ -23,21 +23,24 @@ class RedisCache {
private cacheQueue: MempoolTransactionExtended[] = [];
private txFlushLimit: number = 10000;
constructor() {
if (config.REDIS.ENABLED) {
const redisConfig = {
socket: {
path: config.REDIS.UNIX_SOCKET_PATH
},
database: NetworkDB[config.MEMPOOL.NETWORK],
};
this.client = createClient(redisConfig);
this.client.on('error', (e) => {
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
});
this.$ensureConnected();
}
}
private async $ensureConnected(): Promise<void> {
if (!this.connected && config.REDIS.ENABLED) {
if (!this.client) {
const redisConfig = {
socket: {
path: config.REDIS.UNIX_SOCKET_PATH
},
database: NetworkDB[config.MEMPOOL.NETWORK],
};
this.client = createClient(redisConfig);
this.client.on('error', (e) => {
logger.err(`Error in Redis client: ${e instanceof Error ? e.message : e}`);
});
}
return this.client.connect().then(async () => {
this.connected = true;
logger.info(`Redis client connected`);
+2 -1
View File
@@ -29,9 +29,10 @@ class Statistics {
}
private async runStatistics(): Promise<void> {
if (!memPool.isInSync()) {
if (!memPool.isInSync() || memPool.getStatisticsIsPaused()) {
return;
}
const currentMempool = memPool.getMempool();
const txPerSecond = memPool.getTxPerSecond();
const vBytesPerSecond = memPool.getVBytesPerSecond();
+7 -2
View File
@@ -5,6 +5,7 @@ import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
import * as bitcoinjs from 'bitcoinjs-lib';
import logger from '../logger';
import config from '../config';
import pLimit from '../utils/p-limit';
class TransactionUtils {
constructor() { }
@@ -74,8 +75,12 @@ class TransactionUtils {
public async $getMempoolTransactionsExtended(txids: string[], addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<MempoolTransactionExtended[]> {
if (forceCore || config.MEMPOOL.BACKEND !== 'esplora') {
const results = await Promise.allSettled(txids.map(txid => this.$getTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore, true)));
return (results.filter(r => r.status === 'fulfilled') as PromiseFulfilledResult<MempoolTransactionExtended>[]).map(r => r.value);
const limiter = pLimit(8); // Run 8 requests at a time
const results = await Promise.allSettled(txids.map(
txid => limiter(() => this.$getMempoolTransactionExtended(txid, addPrevouts, lazyPrevouts, forceCore))
));
return results.filter(reply => reply.status === 'fulfilled')
.map(r => (r as PromiseFulfilledResult<MempoolTransactionExtended>).value);
} else {
const transactions = await bitcoinApi.$getMempoolTransactions(txids);
return transactions.map(transaction => {
+30 -9
View File
@@ -71,9 +71,8 @@ class WebsocketHandler {
private updateSocketData(): void {
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
const da = difficultyAdjustment.getDifficultyAdjustment();
this.updateSocketDataFields({
const socketData = {
'mempoolInfo': memPool.getMempoolInfo(),
'vBytesPerSecond': memPool.getVBytesPerSecond(),
'blocks': _blocks,
'conversions': priceUpdater.getLatestPrices(),
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
@@ -82,7 +81,11 @@ class WebsocketHandler {
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': da?.previousTime ? da : undefined,
'fees': feeApi.getRecommendedFee(),
});
};
if (!memPool.getStatisticsIsPaused()) {
socketData['vBytesPerSecond'] = memPool.getVBytesPerSecond();
}
this.updateSocketDataFields(socketData);
}
public getSerializedInitData(): string {
@@ -414,7 +417,7 @@ class WebsocketHandler {
const mBlocks = mempoolBlocks.getMempoolBlocks();
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond();
const vBytesPerSecond = memPool.getStatisticsIsPaused() ? null : memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment();
memPool.handleRbfTransactions(rbfTransactions);
@@ -440,13 +443,15 @@ class WebsocketHandler {
// update init data
const socketDataFields = {
'mempoolInfo': mempoolInfo,
'vBytesPerSecond': vBytesPerSecond,
'mempool-blocks': mBlocks,
'transactions': latestTransactions,
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': da?.previousTime ? da : undefined,
'fees': recommendedFees,
};
if (vBytesPerSecond != null) {
socketDataFields['vBytesPerSecond'] = vBytesPerSecond;
}
if (rbfSummary) {
socketDataFields['rbfSummary'] = rbfSummary;
}
@@ -496,7 +501,9 @@ class WebsocketHandler {
if (client['want-stats']) {
response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', vBytesPerSecond);
if (vBytesPerSecond != null) {
response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', vBytesPerSecond);
}
response['transactions'] = getCachedResponse('transactions', latestTransactions);
if (da?.previousTime) {
response['da'] = getCachedResponse('da', da);
@@ -586,13 +593,25 @@ class WebsocketHandler {
const mempoolTx = newMempool[trackTxid];
if (mempoolTx && mempoolTx.position) {
response['txPosition'] = JSON.stringify({
const positionData = {
txid: trackTxid,
position: {
...mempoolTx.position,
accelerated: mempoolTx.acceleration || undefined,
}
});
};
if (mempoolTx.cpfpDirty) {
positionData['cpfp'] = {
ancestors: mempoolTx.ancestors,
bestDescendant: mempoolTx.bestDescendant || null,
descendants: mempoolTx.descendants || null,
effectiveFeePerVsize: mempoolTx.effectiveFeePerVsize || null,
sigops: mempoolTx.sigops,
adjustedVsize: mempoolTx.adjustedVsize,
acceleration: mempoolTx.acceleration
};
}
response['txPosition'] = JSON.stringify(positionData);
}
}
@@ -772,7 +791,9 @@ class WebsocketHandler {
if (client['want-stats']) {
response['mempoolInfo'] = getCachedResponse('mempoolInfo', mempoolInfo);
response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', memPool.getVBytesPerSecond());
if (!memPool.getStatisticsIsPaused()) {
response['vBytesPerSecond'] = getCachedResponse('vBytesPerSecond', memPool.getVBytesPerSecond());
}
response['fees'] = getCachedResponse('fees', fees);
if (da?.previousTime) {
@@ -1,38 +0,0 @@
import { parentPort } from 'worker_threads';
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
import blocks from '../api/blocks';
import config from '../config';
import transactionUtils from '../api/transaction-utils';
import bitcoinClient from '../api/bitcoin/bitcoin-client';
if (parentPort) {
parentPort.on('message', async ({ hash, height }) => {
if (hash != null && height != null) {
await indexBlockSummary(hash, height);
}
if (parentPort) {
parentPort.postMessage(height);
}
});
}
async function indexBlockSummary(hash: string, height: number): Promise<void> {
let txs;
if (config.MEMPOOL.BACKEND === 'esplora') {
txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
} else {
const block = await bitcoinClient.getBlock(hash, 2);
txs = block.tx.map(tx => {
tx.fee = Math.round(tx.fee * 100_000_000);
tx.vout.forEach((vout) => {
vout.value = Math.round(vout.value * 100000000);
});
tx.vsize = Math.round(tx.weight / 4); // required for backwards compatibility
return tx;
});
}
const cpfpSummary = await blocks.$indexCPFP(hash, height, txs);
await blocks.$getStrippedBlockTransactions(hash, true, true, cpfpSummary, height); // This will index the block summary
}
-25
View File
@@ -1,25 +0,0 @@
import { parentPort } from 'worker_threads';
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
import blocksRepository from '../repositories/BlocksRepository';
import blocks from '../api/blocks';
import { IEsploraApi } from '../api/bitcoin/esplora-api.interface';
if (parentPort) {
parentPort.on('message', async (params) => {
if (params.height != null) {
await indexBlock(params.height);
}
if (parentPort) {
parentPort.postMessage(params.height);
}
});
}
async function indexBlock(blockHeight: number): Promise<void> {
const blockHash = await bitcoinApi.$getBlockHash(blockHeight);
const block: IEsploraApi.Block = await bitcoinApi.$getBlock(blockHash);
const transactions = await blocks['$getTransactionsExtended'](blockHash, block.height, true, null, true);
const blockExtended = await blocks['$getBlockExtended'](block, transactions);
await blocksRepository.$saveBlockInDatabase(blockExtended);
}
+5 -4
View File
@@ -43,7 +43,6 @@ import { AxiosError } from 'axios';
import v8 from 'v8';
import { formatBytes, getBytesUnit } from './utils/format';
import redisCache from './api/redis-cache';
import rbfCache from './api/rbf-cache';
class Server {
private wss: WebSocket.Server | undefined;
@@ -108,8 +107,6 @@ class Server {
}
}
rbfCache.init();
this.app
.use((req: Request, res: Response, next: NextFunction) => {
res.setHeader('Access-Control-Allow-Origin', '*');
@@ -194,10 +191,14 @@ class Server {
logger.debug(msg);
}
}
const newMempool = await bitcoinApi.$getRawMempool();
const { txids: newMempool, local: fromLocalNode } = await bitcoinApi.$getRawMempool();
const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerRunning ? 10 : 1);
if (numHandledBlocks === 0) {
if (!fromLocalNode) {
memPool.logFailover();
}
await memPool.$updateMempool(newMempool, pollRate);
}
indexer.$run();
+1
View File
@@ -104,6 +104,7 @@ export interface MempoolTransactionExtended extends TransactionExtended {
adjustedFeePerVsize: number;
inputs?: number[];
lastBoosted?: number;
cpfpDirty?: boolean;
}
export interface AuditTransaction {
+179
View File
@@ -0,0 +1,179 @@
/*
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
How it works:
`this._head` is an instance of `Node` which keeps track of its current value and nests
another instance of `Node` that keeps the value that comes after it. When a value is
provided to `.enqueue()`, the code needs to iterate through `this._head`, going deeper
and deeper to find the last value. However, iterating through every single item is slow.
This problem is solved by saving a reference to the last value as `this._tail` so that
it can reference it to add a new value.
*/
class Node {
value;
next;
constructor(value) {
this.value = value;
}
}
class Queue {
private _head;
private _tail;
private _size;
constructor() {
this.clear();
}
enqueue(value) {
const node = new Node(value);
if (this._head) {
this._tail.next = node;
this._tail = node;
} else {
this._head = node;
this._tail = node;
}
this._size++;
}
dequeue() {
const current = this._head;
if (!current) {
return;
}
this._head = this._head.next;
this._size--;
return current.value;
}
clear() {
this._head = undefined;
this._tail = undefined;
this._size = 0;
}
get size() {
return this._size;
}
*[Symbol.iterator]() {
let current = this._head;
while (current) {
yield current.value;
current = current.next;
}
}
}
interface LimitFunction {
readonly activeCount: number;
readonly pendingCount: number;
clearQueue: () => void;
<Arguments extends unknown[], ReturnType>(
fn: (...args: Arguments) => PromiseLike<ReturnType> | ReturnType,
...args: Arguments
): Promise<ReturnType>;
}
export default function pLimit(concurrency: number): LimitFunction {
if (
!(
(Number.isInteger(concurrency) ||
concurrency === Number.POSITIVE_INFINITY) &&
concurrency > 0
)
) {
throw new TypeError('Expected `concurrency` to be a number from 1 and up');
}
const queue = new Queue();
let activeCount = 0;
const next = () => {
activeCount--;
if (queue.size > 0) {
queue.dequeue()();
}
};
const run = async (fn, resolve, args) => {
activeCount++;
const result = (async () => fn(...args))();
resolve(result);
try {
await result;
} catch {}
next();
};
const enqueue = (fn, resolve, args) => {
queue.enqueue(run.bind(undefined, fn, resolve, args));
(async () => {
// This function needs to wait until the next microtask before comparing
// `activeCount` to `concurrency`, because `activeCount` is updated asynchronously
// when the run function is dequeued and called. The comparison in the if-statement
// needs to happen asynchronously as well to get an up-to-date value for `activeCount`.
await Promise.resolve();
if (activeCount < concurrency && queue.size > 0) {
queue.dequeue()();
}
})();
};
const generator = (fn, ...args) =>
new Promise((resolve) => {
enqueue(fn, resolve, args);
});
Object.defineProperties(generator, {
activeCount: {
get: () => activeCount,
},
pendingCount: {
get: () => queue.size,
},
clearQueue: {
value: () => {
queue.clear();
},
},
});
return generator as any;
}
+8
View File
@@ -112,6 +112,14 @@ PROXY_CONFIG.push(...[
"^/testnet": ""
},
},
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/api/v1/**'],
target: `http://127.0.0.1:8999`,
+8
View File
@@ -112,6 +112,14 @@ PROXY_CONFIG.push(...[
"^/testnet": ""
},
},
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/api/v1/**'],
target: `http://localhost:8999`,
+8
View File
@@ -95,6 +95,14 @@ if (configContent && configContent.BASE_MODULE === 'bisq') {
}
PROXY_CONFIG.push(...[
{
context: ['/api/v1/services/**'],
target: `http://localhost:9000`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/api/v1/**'],
target: `http://localhost:8999`,
@@ -41,6 +41,7 @@ export class BisqAddressComponent implements OnInit, OnDestroy {
document.body.scrollTo(0, 0);
this.addressString = params.get('id') || '';
this.seoService.setTitle($localize`:@@bisq-address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.address:See current balance, pending transactions, and history of confirmed transactions for BSQ address ${this.addressString}:INTERPOLATION:.`);
return this.bisqApiService.getAddress$(this.addressString)
.pipe(
@@ -88,6 +88,7 @@ export class BisqBlockComponent implements OnInit, OnDestroy {
this.isLoading = false;
this.blockHeight = block.height;
this.seoService.setTitle($localize`:@@bisq-block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.hash}:BLOCK_HASH:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.block:See all BSQ transactions in Bitcoin block ${block.height}:BLOCK_HEIGHT: (block hash ${block.hash}:BLOCK_HASH:).`);
this.block = block;
});
}
@@ -36,6 +36,7 @@ export class BisqBlocksComponent implements OnInit {
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
this.seoService.setDescription($localize`:@@meta.description.bisq.blocks:See a list of recent Bitcoin blocks with BSQ transactions, total BSQ sent per block, and more.`);
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.loadingItems = Array(this.itemsPerPage);
if (document.body.clientWidth < 670) {
@@ -29,7 +29,8 @@ export class BisqDashboardComponent implements OnInit {
) { }
ngOnInit(): void {
this.seoService.setTitle(`Markets`);
this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`);
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Project™. See Bisq market prices, trading activity, and more.`);
this.websocketService.want(['blocks']);
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
@@ -34,6 +34,7 @@ export class BisqMainDashboardComponent implements OnInit {
ngOnInit(): void {
this.seoService.resetTitle();
this.seoService.resetDescription();
this.websocketService.want(['blocks']);
this.usdPrice$ = this.stateService.conversions$.asObservable().pipe(
@@ -48,7 +48,8 @@ export class BisqMarketComponent implements OnInit, OnDestroy {
map(([markets, routeParams]) => {
const pair = routeParams.get('pair');
const pairUpperCase = pair.replace('_', '/').toUpperCase();
this.seoService.setTitle(`Bisq market: ${pairUpperCase}`);
this.seoService.setTitle($localize`:@@meta.title.bisq.market:Bisq market: ${pairUpperCase}`);
this.seoService.setDescription($localize`:@@meta.description.bisq.market:See price history, current buy/sell offers, and latest trades for the ${pairUpperCase} market on Bisq.`);
return {
pair: pairUpperCase,
@@ -26,6 +26,7 @@ export class BisqStatsComponent implements OnInit {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@2a30a4cdb123a03facc5ab8c5b3e6d8b8dbbc3d4:BSQ statistics`);
this.seoService.setDescription($localize`:@@meta.description.bisq.stats:See high-level stats on the BSQ economy: supply metrics, number of addresses, BSQ price, market cap, and more.`);
this.stateService.bsqPrice$
.subscribe((bsqPrice) => {
this.price = bsqPrice;
@@ -48,6 +48,7 @@ export class BisqTransactionComponent implements OnInit, OnDestroy {
document.body.scrollTo(0, 0);
this.txId = params.get('id') || '';
this.seoService.setTitle($localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.transaction:See inputs, outputs, transaction type, burnt amount, and more for transaction with txid ${this.txId}:INTERPOLATION:.`);
if (history.state.data) {
return of(history.state.data);
}
@@ -79,6 +79,7 @@ export class BisqTransactionsComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@add4cd82e3e38a3110fe67b3c7df56e9602644ee:Transactions`);
this.seoService.setDescription($localize`:@@meta.description.bisq.transactions:See recent BSQ transactions: amount, txid, associated Bitcoin block, transaction type, and more.`);
this.radioGroupForm = this.formBuilder.group({
txTypes: [this.txTypesDefaultChecked],
@@ -4,7 +4,8 @@
<span style="margin-left: auto; margin-right: -20px; margin-bottom: -20px">&reg;</span>
<img class="logo" src="/resources/mempool-logo-bigger.png" />
<div class="version">
v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]
<span>v{{ packetJsonVersion }} [<a href="https://github.com/mempool/mempool/commit/{{ frontendGitCommitHash }}">{{ frontendGitCommitHash }}</a>]</span>
<span *ngIf="stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE">[{{ stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE }}]</span>
</div>
</div>
@@ -242,7 +243,7 @@
<img class="image" src="/resources/profile/ronindojo.png" />
<span>RoninDojo</span>
</a>
<a href="https://github.com/runcitadel/core" target="_blank" title="Citadel">
<a href="https://github.com/runcitadel" target="_blank" title="Citadel">
<img class="image" src="/resources/profile/runcitadel.svg" />
<span>Citadel</span>
</a>
@@ -22,6 +22,7 @@
.intro {
margin: 25px auto 30px;
margin-top: 25px;
width: 250px;
display: flex;
flex-direction: column;
@@ -43,6 +43,7 @@ export class AboutComponent implements OnInit {
ngOnInit() {
this.backendInfo$ = this.stateService.backendInfo$;
this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`);
this.seoService.setDescription($localize`:@@meta.description.about:Learn more about The Mempool Open Source Project™\: enterprise sponsors, individual sponsors, integrations, who contributes, FOSS licensing, and more.`);
this.websocketService.want(['blocks']);
this.profiles$ = this.apiService.getAboutPageProfiles$().pipe(
@@ -0,0 +1,21 @@
<div class="fee-graph" *ngIf="tx && estimate">
<div class="column">
<ng-container *ngFor="let bar of bars">
<div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);">
<div class="fill"></div>
<div class="line">
<p class="fee-rate">
<span class="label">{{ bar.label }}</span>
<span class="rate">
<app-fee-rate [fee]="bar.rate"></app-fee-rate>
</span>
</p>
</div>
<div class="spacer"></div>
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }} {{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
<div class="spacer"></div>
<div class="spacer"></div>
</div>
</ng-container>
</div>
</div>
@@ -0,0 +1,157 @@
.fee-graph {
height: 100%;
min-width: 120px;
width: 120px;
max-height: 90vh;
margin-left: 4em;
margin-right: 1.5em;
padding-bottom: 63px;
.column {
width: 100%;
height: 100%;
position: relative;
background: #181b2d;
.bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.fill {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
opacity: 0.75;
pointer-events: none;
}
.fee {
font-size: 0.9em;
opacity: 0;
pointer-events: none;
}
.spacer {
width: 100%;
height: 1px;
flex-grow: 1;
pointer-events: none;
}
.line {
position: absolute;
right: 0;
top: 0;
left: -4.5em;
border-top: dashed white 1.5px;
.fee-rate {
width: 100%;
position: absolute;
left: 0;
right: 0.2em;
font-size: 0.8em;
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
margin: 0;
.label {
margin-right: .2em;
}
.rate .symbol {
color: white;
}
}
}
&.tx {
.fill {
background: #3bcc49;
}
.line {
.fee-rate {
top: 0;
}
}
.fee {
position: absolute;
opacity: 1;
z-index: 11;
}
}
&.target {
.fill {
background: #653b9c;
}
.fee {
position: absolute;
opacity: 1;
z-index: 11;
}
.line .fee-rate {
bottom: 2px;
}
}
&.max {
cursor: pointer;
.line .fee-rate {
.label {
opacity: 0;
}
bottom: 2px;
}
&.active, &:hover {
.fill {
background: #105fb0;
}
.line {
.fee-rate .label {
opacity: 1;
}
}
}
}
&:hover {
.fill {
z-index: 10;
}
.line {
z-index: 11;
}
.fee {
opacity: 1;
z-index: 12;
}
}
}
&:hover > .bar:not(:hover) {
&.target, &.max {
.fee {
opacity: 0;
}
.line .fee-rate .label {
opacity: 0;
}
}
&.max {
.fill {
background: none;
}
}
}
}
}
@@ -0,0 +1,96 @@
import { Component, OnInit, Input, Output, OnChanges, EventEmitter, HostListener, Inject, LOCALE_ID } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
import { Router } from '@angular/router';
import { ReplaySubject, merge, Subscription, of } from 'rxjs';
import { tap, switchMap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { AccelerationEstimate, RateOption } from './accelerate-preview.component';
interface GraphBar {
rate: number;
style: any;
class: 'tx' | 'target' | 'max';
label: string;
active?: boolean;
rateIndex?: number;
fee?: number;
}
@Component({
selector: 'app-accelerate-fee-graph',
templateUrl: './accelerate-fee-graph.component.html',
styleUrls: ['./accelerate-fee-graph.component.scss'],
})
export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
@Input() tx: Transaction;
@Input() estimate: AccelerationEstimate;
@Input() maxRateOptions: RateOption[] = [];
@Input() maxRateIndex: number = 0;
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
bars: GraphBar[] = [];
tooltipPosition = { x: 0, y: 0 };
ngOnInit(): void {
this.initGraph();
}
ngOnChanges(): void {
this.initGraph();
}
initGraph(): void {
if (!this.tx || !this.estimate) {
return;
}
const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate));
const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize;
const baseHeight = baseRate / maxRate;
const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => {
return {
rate: option.rate,
style: this.getStyle(option.rate, maxRate, baseHeight),
class: 'max',
label: 'maximum',
active: option.index === this.maxRateIndex,
rateIndex: option.index,
fee: option.fee,
}
});
bars.push({
rate: this.estimate.targetFeeRate,
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight),
class: 'target',
label: 'next block',
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
});
bars.push({
rate: baseRate,
style: this.getStyle(baseRate, maxRate, 0),
class: 'tx',
label: '',
fee: this.estimate.txSummary.effectiveFee,
});
this.bars = bars;
}
getStyle(rate, maxRate, base) {
const top = (rate / maxRate);
return {
height: `${(top - base) * 100}%`,
bottom: base ? `${base * 100}%` : '0',
}
}
onClick(event, bar): void {
if (bar.rateIndex != null) {
this.setUserBid.emit({ fee: bar.fee, index: bar.rateIndex });
}
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
}
}
@@ -0,0 +1,262 @@
<div class="row" *ngIf="showSuccess">
<div class="col" id="successAlert">
<div class="alert alert-success">
Transaction has now been submitted to mining pools for acceleration. You can track the progress <a class="alert-link" routerLink="/services/accelerator/history">here</a>.
</div>
</div>
</div>
<div class="row" *ngIf="error">
<div class="col" id="mempoolError">
<app-mempool-error [error]="error"></app-mempool-error>
</div>
</div>
<div class="accelerate-cols">
<ng-container *ngIf="!isMobile">
<app-accelerate-fee-graph
[tx]="tx"
[estimate]="estimate"
[maxRateOptions]="maxRateOptions"
[maxRateIndex]="selectFeeRateIndex"
(setUserBid)="setUserBid($event)"
></app-accelerate-fee-graph>
</ng-container>
<ng-container *ngIf="estimate">
<div [class]="{estimateDisabled: error}">
<h5>Your transaction</h5>
<div class="row">
<div class="col">
<small *ngIf="hasAncestors" class="form-text text-muted mb-2">
Plus {{ estimate.txSummary.ancestorCount - 1 }} unconfirmed ancestor{{ estimate.txSummary.ancestorCount > 2 ? 's' : ''}}.
</small>
<table class="table table-borderless table-border table-dark table-accelerator">
<tbody>
<tr class="group-first">
<td class="item">
Virtual size
</td>
<td class="units" [innerHTML]="'&lrm;' + (estimate.txSummary.effectiveVsize | vbytes: 2)"></td>
</tr>
<tr class="info">
<td class="info">
<i><small>Size in vbytes of this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
</td>
</tr>
<tr>
<td class="item">
In-band fees
</td>
<td class="units">
{{ estimate.txSummary.effectiveFee | number : '1.0-0' }} <span class="symbol" i18n="shared.sats|sats">sats</span>
</td>
</tr>
<tr class="info group-last">
<td class="info">
<i><small>Fees already paid by this transaction<span *ngIf="hasAncestors"> and its unconfirmed ancestors</span></small></i>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<br>
<h5>How much more are you willing to pay?</h5>
<div class="row">
<div class="col">
<small class="form-text text-muted mb-2">
Choose the maximum extra transaction fee you're willing to pay to get into the next block.<br>
If the estimated next block rate rises beyond this limit, we will automatically cancel your acceleration request.
</small>
<div class="form-group">
<div class="fee-card">
<div class="d-flex mb-0">
<ng-container *ngFor="let option of maxRateOptions">
<button type="button" class="btn btn-primary flex-grow-1 btn-border btn-sm feerate" [class]="{active: selectFeeRateIndex === option.index}" (click)="setUserBid(option)">
<span class="fee">{{ option.fee | number }} <span class="symbol" i18n="shared.sats|sats">sats</span></span>
<span class="rate">~ <app-fee-rate [fee]="option.rate" rounding="1.0-0"></app-fee-rate></span>
</button>
</ng-container>
</div>
</div>
</div>
</div>
</div>
<h5>Acceleration summary</h5>
<div class="row mb-3">
<div class="col">
<div class="table-toggle btn-group btn-group-toggle">
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'estimated'" (click)="showTable = 'estimated'">
<span>Estimated cost</span>
</div>
<div class="btn btn-primary btn-sm" [class.active]="showTable === 'maximum'" (click)="showTable = 'maximum'">
<span>Maximum cost</span>
</div>
</div>
<table class="table table-borderless table-border table-dark table-accelerator">
<tbody>
<!-- ESTIMATED FEE -->
<ng-container *ngIf="showTable === 'estimated'">
<tr class="group-first">
<td class="item">
Next block market rate
</td>
<td class="amt" style="font-size: 20px">
{{ estimate.targetFeeRate | number : '1.0-0' }}
</td>
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
</tr>
<tr class="info">
<td class="info">
<i><small>Estimated extra fee required</small></i>
</td>
<td class="amt">
{{ math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee) | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="math.max(0, estimate.nextBlockFee - estimate.txSummary.effectiveFee)"></app-fiat></span>
</td>
</tr>
</ng-container>
<!-- USER MAX BID -->
<ng-container *ngIf="showTable === 'maximum'">
<tr class="group-first">
<td class="item">
Your maximum
</td>
<td class="amt" style="width: 45%; font-size: 20px">
~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }}
</td>
<td class="units"><span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></td>
</tr>
<tr class="info">
<td class="info">
<i><small>The maximum extra transaction fee you could pay</small></i>
</td>
<td class="amt">
<span>
{{ userBid | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="userBid"></app-fiat></span>
</td>
</tr>
</ng-container>
<!-- MEMPOOL BASE FEE -->
<tr>
<td class="item">
Mempool Accelerator™ fees
</td>
</tr>
<tr class="info">
<td class="info">
<i><small>mempool.space fee</small></i>
</td>
<td class="amt">
+{{ estimate.mempoolBaseFee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
<td class="info">
<i><small>Transaction vsize fee</small></i>
</td>
<td class="amt">
+{{ estimate.vsizeFee | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
<!-- NEXT BLOCK ESTIMATE -->
<ng-container *ngIf="showTable === 'estimated'">
<tr class="group-first">
<td class="item">
<b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b>
</td>
<td class="amt">
<span style="background-color: #5E35B1" class="p-1 pl-0">
{{ estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last">
<td class="info">
<i><small>If your tx is accelerated to </small><small>{{ estimate.targetFeeRate | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
</td>
</tr>
</ng-container>
<!-- MAX COST -->
<ng-container *ngIf="showTable === 'maximum'">
<tr class="group-first">
<td class="item">
<b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b>
</td>
<td class="amt">
<span style="background-color: #105fb0" class="p-1 pl-0">
{{ maxCost | number }}
</span>
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat">
<app-fiat [value]="maxCost" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
<tr class="info group-last">
<td class="info">
<i><small>If your tx is accelerated to </small><small>~{{ ((estimate.txSummary.effectiveFee + userBid) / estimate.txSummary.effectiveVsize) | number : '1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></small></i>
</td>
</tr>
</ng-container>
<!-- USER BALANCE -->
<ng-container *ngIf="estimate.userBalance < maxCost">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item">
Available balance
</td>
<td class="amt">
{{ estimate.userBalance | number }}
</td>
<td class="units">
<span class="symbol" i18n="shared.sats|sats">sats</span>
<span class="fiat">
<app-fiat [value]="estimate.userBalance" [colorClass]="estimate.userBalance < maxCost ? 'red-color' : 'green-color'"></app-fiat>
</span>
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>
</div>
<div class="row mb-3" *ngIf="isLoggedIn()">
<div class="col">
<div class="d-flex justify-content-end">
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()">Accelerate</button>
</div>
</div>
</div>
</div>
</ng-container>
</div>
@@ -0,0 +1,88 @@
.fee-card {
padding: 15px;
background-color: #1d1f31;
.feerate {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.fee {
font-size: 1.2em;
}
.rate {
font-size: 0.9em;
.symbol {
color: white;
}
}
}
}
.btn-border {
border: solid 1px black;
background-color: #0c4a87;
}
.feerate.active {
background-color: #105fb0 !important;
opacity: 1;
border: 1px solid white !important;
}
.estimateDisabled {
opacity: 0.5;
pointer-events: none;
}
.table-toggle {
width: 100%;
margin-top: 0.5em;
}
.table-accelerator {
tr {
text-wrap: wrap;
td {
padding-top: 0;
padding-bottom: 0;
vertical-align: baseline;
}
&.group-first {
td {
padding-top: 0.75rem;
}
}
&.group-last {
td {
padding-bottom: 0.75rem;
}
}
}
td {
&:first-child {
width: 100vw;
}
&.info {
color: #6c757d;
}
&.amt {
text-align: right;
padding-right: 0.2em;
}
&.units {
padding-left: 0.2em;
white-space: nowrap;
}
}
}
.accelerate-cols {
display: flex;
flex-direction: row;
align-items: stretch;
margin-top: 1em;
}
@@ -0,0 +1,205 @@
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener } from '@angular/core';
import { ApiService } from '../../services/api.service';
import { Subscription, catchError, of, tap } from 'rxjs';
import { StorageService } from '../../services/storage.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { nextRoundNumber } from '../../shared/common.utils';
export type AccelerationEstimate = {
txSummary: TxSummary;
nextBlockFee: number;
targetFeeRate: number;
userBalance: number;
enoughBalance: boolean;
cost: number;
mempoolBaseFee: number;
vsizeFee: number;
}
export type TxSummary = {
txid: string; // txid of the current transaction
effectiveVsize: number; // Total vsize of the dependency tree
effectiveFee: number; // Total fee of the dependency tree in sats
ancestorCount: number; // Number of ancestors
}
export interface RateOption {
fee: number;
rate: number;
index: number;
}
export const MIN_BID_RATIO = 1;
export const DEFAULT_BID_RATIO = 2;
export const MAX_BID_RATIO = 4;
@Component({
selector: 'app-accelerate-preview',
templateUrl: 'accelerate-preview.component.html',
styleUrls: ['accelerate-preview.component.scss']
})
export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges {
@Input() tx: Transaction | undefined;
@Input() scrollEvent: boolean;
math = Math;
error = '';
showSuccess = false;
estimateSubscription: Subscription;
accelerationSubscription: Subscription;
estimate: any;
hasAncestors: boolean = false;
minExtraCost = 0;
minBidAllowed = 0;
maxBidAllowed = 0;
defaultBid = 0;
maxCost = 0;
userBid = 0;
selectFeeRateIndex = 1;
showTable: 'estimated' | 'maximum' = 'maximum';
isMobile: boolean = window.innerWidth <= 767.98;
maxRateOptions: RateOption[] = [];
constructor(
private apiService: ApiService,
private storageService: StorageService
) { }
ngOnDestroy(): void {
if (this.estimateSubscription) {
this.estimateSubscription.unsubscribe();
}
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.scrollEvent) {
this.scrollToPreview('acceleratePreviewAnchor', 'center');
}
}
ngOnInit() {
this.estimateSubscription = this.apiService.estimate$(this.tx.txid).pipe(
tap((response) => {
if (response.status === 204) {
this.estimate = undefined;
this.error = `cannot_accelerate_tx`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
} else {
this.estimate = response.body;
if (!this.estimate) {
this.error = `cannot_accelerate_tx`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
}
if (this.estimate.userBalance <= 0) {
if (this.isLoggedIn()) {
this.error = `not_enough_balance`;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
}
}
this.hasAncestors = this.estimate.txSummary.ancestorCount > 1;
// Make min extra fee at least 50% of the current tx fee
this.minExtraCost = nextRoundNumber(Math.max(this.estimate.cost * 2, this.estimate.txSummary.effectiveFee));
this.maxRateOptions = [1, 2, 4].map((multiplier, index) => {
return {
fee: this.minExtraCost * multiplier,
rate: (this.estimate.txSummary.effectiveFee + (this.minExtraCost * multiplier)) / this.estimate.txSummary.effectiveVsize,
index,
};
});
this.minBidAllowed = this.minExtraCost * MIN_BID_RATIO;
this.defaultBid = this.minExtraCost * DEFAULT_BID_RATIO;
this.maxBidAllowed = this.minExtraCost * MAX_BID_RATIO;
this.userBid = this.defaultBid;
if (this.userBid < this.minBidAllowed) {
this.userBid = this.minBidAllowed;
} else if (this.userBid > this.maxBidAllowed) {
this.userBid = this.maxBidAllowed;
}
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
if (!this.error) {
this.scrollToPreview('acceleratePreviewAnchor', 'center');
}
}
}),
catchError((response) => {
this.estimate = undefined;
this.error = response.error;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
this.estimateSubscription.unsubscribe();
return of(null);
})
).subscribe();
}
/**
* User changed his bid
*/
setUserBid({ fee, index }: { fee: number, index: number}) {
if (this.estimate) {
this.selectFeeRateIndex = index;
this.userBid = Math.max(0, fee);
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
}
}
/**
* Scroll to element id with or without setTimeout
*/
scrollToPreviewWithTimeout(id: string, position: ScrollLogicalPosition) {
setTimeout(() => {
this.scrollToPreview(id, position);
}, 100);
}
scrollToPreview(id: string, position: ScrollLogicalPosition) {
const acceleratePreviewAnchor = document.getElementById(id);
if (acceleratePreviewAnchor) {
acceleratePreviewAnchor.scrollIntoView({
behavior: 'smooth',
inline: position,
block: position,
});
}
}
/**
* Send acceleration request
*/
accelerate() {
if (this.accelerationSubscription) {
this.accelerationSubscription.unsubscribe();
}
this.accelerationSubscription = this.apiService.accelerate$(
this.tx.txid,
this.userBid
).subscribe({
next: () => {
this.showSuccess = true;
this.scrollToPreviewWithTimeout('successAlert', 'center');
this.estimateSubscription.unsubscribe();
},
error: (response) => {
this.error = response.error;
this.scrollToPreviewWithTimeout('mempoolError', 'center');
}
});
}
isLoggedIn() {
const auth = this.storageService.getAuth();
return auth !== null;
}
@HostListener('window:resize', ['$event'])
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
}
}
@@ -9,6 +9,7 @@ import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { of, merge, Subscription, Observable } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface';
@Component({
@@ -68,6 +69,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
this.addressString = this.addressString.toLowerCase();
}
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
? this.electrsApiService.getPubKeyAddress$(this.addressString)
@@ -9,6 +9,7 @@ import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { of, merge, Subscription, Observable } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface';
@Component({
@@ -76,6 +77,7 @@ export class AddressComponent implements OnInit, OnDestroy {
this.addressString = this.addressString.toLowerCase();
}
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
return merge(
of(true),
@@ -40,6 +40,7 @@ export class AssetsNavComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@ee8f8008bae6ce3a49840c4e1d39b4af23d4c263:Assets`);
this.seoService.setDescription($localize`:@@meta.description.liquid.assets:Explore all the assets issued on the Liquid network like L-BTC, L-CAD, USDT, and more.`);
this.typeaheadSearchFn = this.typeaheadSearch;
this.searchForm = this.formBuilder.group({
@@ -64,6 +64,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fee-rates:See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles.`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@@ -65,6 +65,7 @@ export class BlockFeesGraphComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Block Fees`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fees:See the average mining fees earned per Bitcoin block visualized in BTC and USD over time.`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('1m');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@@ -192,7 +193,7 @@ export class BlockFeesGraphComponent implements OnInit {
{
name: 'Fees ' + this.currency,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
textStyle: {
color: 'white',
},
icon: 'roundRect',
@@ -61,6 +61,7 @@ export class BlockHealthGraphComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@d7d5fcf50179ad70c938491c517efb82de2c8146:Block Health`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-health:See Bitcoin block health visualized over time. Block health is a measure of how many expected transactions were included in an actual mined block. Expected transactions are determined using Mempool's re-implementation of Bitcoin Core's transaction selection algorithm.`);
this.miningWindowPreference = '24h';//this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@@ -63,6 +63,7 @@ export class BlockRewardsGraphComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@8ba8fe810458280a83df7fdf4c614dfc1a826445:Block Rewards`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-rewards:See Bitcoin block rewards in BTC and USD visualized over time. Block rewards are the total funds miners earn from the block subsidy and fees.`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('3m');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@@ -191,7 +192,7 @@ export class BlockRewardsGraphComponent implements OnInit {
{
name: 'Rewards ' + this.currency,
inactiveColor: 'rgb(110, 112, 121)',
textStyle: {
textStyle: {
color: 'white',
},
icon: 'roundRect',
@@ -60,6 +60,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
let firstRun = true;
this.seoService.setTitle($localize`:@@56fa1cd221491b6478998679cba2dc8d55ba330d:Block Sizes and Weights`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-sizes:See Bitcoin block sizes (MB) and block weights (weight units) visualized over time.`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@@ -8,6 +8,7 @@ import { SeoService } from '../../services/seo.service';
import { OpenGraphService } from '../../services/opengraph.service';
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
@Component({
@@ -97,6 +98,11 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
this.blockHeight = block.height;
this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) {
this.seoService.setDescription($localize`:@@meta.description.liquid.block:See size, weight, fee range, included transactions, and more for Liquid${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
} else {
this.seoService.setDescription($localize`:@@meta.description.bitcoin.block:See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
}
this.isLoadingBlock = false;
this.setBlockSubsidy();
if (block?.extras?.reward !== undefined) {
@@ -13,6 +13,7 @@ import { BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces
import { ApiService } from '../../services/api.service';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
import { detectWebGL } from '../../shared/graphs.utils';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { PriceService, Price } from '../../services/price.service';
import { CacheService } from '../../services/cache.service';
@@ -261,6 +262,11 @@ export class BlockComponent implements OnInit, OnDestroy {
this.setNextAndPreviousBlockLink();
this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) {
this.seoService.setDescription($localize`:@@meta.description.liquid.block:See size, weight, fee range, included transactions, and more for Liquid${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
} else {
this.seoService.setDescription($localize`:@@meta.description.bitcoin.block:See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
}
this.isLoadingBlock = false;
this.setBlockSubsidy();
if (block?.extras?.reward !== undefined) {
@@ -325,7 +331,7 @@ export class BlockComponent implements OnInit, OnDestroy {
]);
})
)
.subscribe(([transactions, blockAudit]) => {
.subscribe(([transactions, blockAudit]) => {
if (transactions) {
this.strippedTransactions = transactions;
} else {
@@ -680,7 +686,7 @@ export class BlockComponent implements OnInit, OnDestroy {
this.setAuditAvailable(false);
}
}
isAuditAvailableFromBlockHeight(blockHeight: number): boolean {
if (!this.auditSupported) {
return false;
@@ -729,4 +735,4 @@ export class BlockComponent implements OnInit, OnDestroy {
this.block.canonical = block.id;
}
}
}
}
@@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, HostListener, ChangeDetectorRef } from '@angular/core';
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, HostListener, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
import { firstValueFrom, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
@@ -8,12 +8,13 @@ import { StateService } from '../../services/state.service';
styleUrls: ['./blockchain.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockchainComponent implements OnInit, OnDestroy {
export class BlockchainComponent implements OnInit, OnDestroy, OnChanges {
@Input() pages: any[] = [];
@Input() pageIndex: number;
@Input() blocksPerPage: number = 8;
@Input() minScrollWidth: number = 0;
@Input() scrollableMempool: boolean = false;
@Input() containerWidth: number;
@Output() mempoolOffsetChange: EventEmitter<number> = new EventEmitter();
@@ -85,19 +86,25 @@ export class BlockchainComponent implements OnInit, OnDestroy {
this.mempoolOffsetChange.emit(this.mempoolOffset);
}
@HostListener('window:resize', ['$event'])
ngOnChanges(changes: SimpleChanges): void {
if (changes.containerWidth) {
this.onResize();
}
}
onResize(): void {
if (window.innerWidth >= 768) {
const width = this.containerWidth || window.innerWidth;
if (width >= 768) {
if (this.stateService.isLiquid()) {
this.dividerOffset = 420;
} else {
this.dividerOffset = window.innerWidth * 0.5;
this.dividerOffset = width * 0.5;
}
} else {
if (this.stateService.isLiquid()) {
this.dividerOffset = window.innerWidth * 0.5;
this.dividerOffset = width * 0.5;
} else {
this.dividerOffset = window.innerWidth * 0.95;
this.dividerOffset = width * 0.95;
}
}
this.cd.markForCheck();
@@ -5,6 +5,8 @@ import { BlockExtended } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
@Component({
selector: 'app-blocks-list',
@@ -35,6 +37,7 @@ export class BlocksList implements OnInit {
private websocketService: WebsocketService,
public stateService: StateService,
private cd: ChangeDetectorRef,
private seoService: SeoService,
) {
}
@@ -50,6 +53,14 @@ export class BlocksList implements OnInit {
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
this.seoService.setTitle($localize`:@@meta.title.blocks-list:Blocks`);
if( this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet' ) {
this.seoService.setDescription($localize`:@@meta.description.liquid.blocks:See the most recent Liquid${seoDescriptionNetwork(this.stateService.network)} blocks along with basic stats such as block height, block size, and more.`);
} else {
this.seoService.setDescription($localize`:@@meta.description.bitcoin.blocks:See the most recent Bitcoin${seoDescriptionNetwork(this.stateService.network)} blocks along with basic stats such as block height, block reward, block size, and more.`);
}
this.blocks$ = combineLatest([
this.fromHeightSubject.pipe(
switchMap((fromBlockHeight) => {
@@ -129,4 +140,4 @@ export class BlocksList implements OnInit {
isEllipsisActive(e): boolean {
return (e.offsetWidth < e.scrollWidth);
}
}
}
@@ -194,7 +194,7 @@ export class DifficultyComponent implements OnInit {
@HostListener('pointerdown', ['$event'])
onPointerDown(event): void {
if (this.epochSvgElement.nativeElement?.contains(event.target)) {
if (this.epochSvgElement?.nativeElement?.contains(event.target)) {
this.onPointerMove(event);
event.preventDefault();
}
@@ -202,7 +202,7 @@ export class DifficultyComponent implements OnInit {
@HostListener('pointermove', ['$event'])
onPointerMove(event): void {
if (this.epochSvgElement.nativeElement?.contains(event.target)) {
if (this.epochSvgElement?.nativeElement?.contains(event.target)) {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
this.cd.markForCheck();
}
@@ -1,5 +1,5 @@
<div [formGroup]="fiatForm" class="text-small text-center">
<select formControlName="fiat" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 150px;" (change)="changeFiat()">
<option *ngFor="let currency of currencies" [value]="currency[1].code">{{ currency[1].name + " (" + currency[1].code + ")" }}</option>
<select formControlName="fiat" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 85px;" (change)="changeFiat()">
<option *ngFor="let currency of currencies" [value]="currency[1].code">{{ currency[1].code }}</option>
</select>
</div>
@@ -12,6 +12,7 @@ import { MiningService } from '../../services/mining.service';
import { download } from '../../shared/graphs.utils';
import { ActivatedRoute } from '@angular/router';
import { StateService } from '../../services/state.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
@Component({
selector: 'app-hashrate-chart',
@@ -71,6 +72,7 @@ export class HashrateChartComponent implements OnInit {
this.miningWindowPreference = '1y';
} else {
this.seoService.setTitle($localize`:@@3510fc6daa1d975f331e3a717bdf1a34efa06dff:Hashrate & Difficulty`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.hashrate:See hashrate and difficulty for the Bitcoin${seoDescriptionNetwork(this.network)} network visualized over time.`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('3m');
}
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
@@ -256,7 +258,7 @@ export class HashrateChartComponent implements OnInit {
let difficultyPowerOfTen = hashratePowerOfTen;
let difficulty = tick.data[1];
if (difficulty === null) {
difficultyString = `${tick.marker} ${tick.seriesName}: No data<br>`;
difficultyString = `${tick.marker} ${tick.seriesName}: No data<br>`;
} else {
if (this.isMobile()) {
difficultyPowerOfTen = selectPowerOfTen(tick.data[1]);
@@ -1,5 +1,5 @@
<div [formGroup]="languageForm" class="text-small text-center">
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 100px;" (change)="changeLanguage()">
<select formControlName="language" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 95px;" (change)="changeLanguage()">
<option *ngFor="let lang of languages" [value]="lang.code">{{ lang.name }}</option>
</select>
</div>
@@ -1,6 +1,20 @@
<ng-container *ngIf="{ val: network$ | async } as network">
<header *ngIf="headerVisible">
<header *ngIf="headerVisible" class="sticky-header">
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<!-- Hamburger -->
<ng-container *ngIf="servicesEnabled">
<div *ngIf="user" class="profile_image_container" [class]="{'anon': !user.imageMd5}" (click)="hamburgerClick($event)">
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/image/' + user.username + '?md5=' + user.imageMd5" class="profile_image">
<app-svg-images style="color: lightgrey; fill: lightgray" *ngIf="!user.imageMd5" name="anon"></app-svg-images>
</div>
<div *ngIf="false && user === null" class="profile_image_container" (click)="hamburgerClick($event)">
<app-svg-images name="hamburger" height="40"></app-svg-images>
</div>
<!-- Empty placeholder -->
<div *ngIf="user === undefined" class="profile_image_container"></div>
</ng-container>
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
<ng-template [ngIf]="subdomain">
<div class="subdomain_container">
@@ -62,11 +76,19 @@
</nav>
</header>
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>
<div class="d-flex" style="overflow: clip">
<app-menu *ngIf="servicesEnabled" [navOpen]="menuOpen" (loggedOut)="onLoggedOut()" (menuToggled)="menuToggled($event)"></app-menu>
<main>
<router-outlet></router-outlet>
</main>
<div class="flex-grow-1 d-flex flex-column">
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>
<main style="min-width: 375px" [style]="menuOpen ? 'max-width: calc(100vw - 225px)' : 'max-width: 100vw'">
<router-outlet></router-outlet>
</main>
<div class="flex-grow-1"></div>
<app-global-footer *ngIf="footerVisible"></app-global-footer>
</div>
</div>
<app-global-footer *ngIf="footerVisible"></app-global-footer>
</ng-container>
@@ -1,3 +1,11 @@
.sticky-header {
position: sticky;
position: -webkit-sticky;
top: 0;
width: 100%;
z-index: 100;
}
li.nav-item.active {
background-color: #653b9c;
}
@@ -86,7 +94,6 @@ li.nav-item {
.navbar-brand {
position: relative;
height: 65px;
}
.navbar-brand.dual-logos {
@@ -102,7 +109,7 @@ nav {
.connection-badge {
position: absolute;
top: 22px;
top: 12px;
width: 100%;
}
@@ -209,4 +216,26 @@ nav {
margin-left: 5px;
margin-right: 0px;
}
}
.profile_image_container {
width: 35px;
margin-right: 15px;
text-align: center;
align-self: center;
cursor: pointer;
&.anon {
border: 1.5px solid lightgrey;
color: lightgrey;
border-radius: 5px;
}
}
.profile_image {
height: 35px;
border-radius: 5px;
}
main {
transition: 0.2s;
transition-property: max-width;
}
@@ -1,9 +1,13 @@
import { Component, OnInit, Input } from '@angular/core';
import { Component, OnInit, Input, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { Env, StateService } from '../../services/state.service';
import { Observable, merge, of } from 'rxjs';
import { LanguageService } from '../../services/language.service';
import { EnterpriseService } from '../../services/enterprise.service';
import { NavigationService } from '../../services/navigation.service';
import { MenuComponent } from '../menu/menu.component';
import { StorageService } from '../../services/storage.service';
import { ApiService } from '../../services/api.service';
@Component({
selector: 'app-master-page',
@@ -25,12 +29,21 @@ export class MasterPageComponent implements OnInit {
networkPaths: { [network: string]: string };
networkPaths$: Observable<Record<string, string>>;
footerVisible = true;
user: any = undefined;
servicesEnabled = false;
menuOpen = false;
@ViewChild(MenuComponent)
public menuComponent!: MenuComponent;
constructor(
public stateService: StateService,
private languageService: LanguageService,
private enterpriseService: EnterpriseService,
private navigationService: NavigationService,
private storageService: StorageService,
private apiService: ApiService,
private router: Router,
) { }
ngOnInit(): void {
@@ -51,17 +64,47 @@ export class MasterPageComponent implements OnInit {
this.footerVisible = this.footerVisibleOverride;
}
});
this.servicesEnabled = this.officialMempoolSpace && this.stateService.env.ACCELERATOR === true && this.stateService.network === '';
this.refreshAuth();
const isServicesPage = this.router.url.includes('/services/');
this.menuOpen = isServicesPage && !this.isSmallScreen();
}
collapse(): void {
this.navCollapsed = !this.navCollapsed;
}
isSmallScreen() {
return window.innerWidth <= 767.98;
}
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
this.isMobile = this.isSmallScreen();
}
brandClick(e): void {
this.stateService.resetScroll$.next(true);
}
onLoggedOut(): void {
this.refreshAuth();
}
refreshAuth(): void {
this.user = this.storageService.getAuth()?.user ?? null;
}
hamburgerClick(event): void {
if (this.menuComponent) {
this.menuComponent.hamburgerClick();
this.menuOpen = this.menuComponent.navOpen;
event.stopPropagation();
}
}
menuToggled(isOpen: boolean): void {
this.menuOpen = isOpen;
}
}
@@ -5,6 +5,7 @@ import { switchMap, map, tap, filter } from 'rxjs/operators';
import { MempoolBlock, TransactionStripped } from '../../interfaces/websocket.interface';
import { Observable, BehaviorSubject } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { WebsocketService } from '../../services/websocket.service';
@Component({
@@ -54,6 +55,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
const ordinal = this.getOrdinal(mempoolBlocks[this.mempoolBlockIndex]);
this.ordinal$.next(ordinal);
this.seoService.setTitle(ordinal);
this.seoService.setDescription($localize`:@@meta.description.mempool-block:See stats for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} transactions in the mempool: fee range, aggregate size, and more. Mempool blocks are updated in real-time as the network receives new transactions.`);
mempoolBlocks[this.mempoolBlockIndex].isStack = mempoolBlocks[this.mempoolBlockIndex].blockVSize > this.stateService.blockVSize;
return mempoolBlocks[this.mempoolBlockIndex];
})
@@ -0,0 +1,31 @@
<div class="sidenav menu-click" [class]="navOpen ? 'open': ''">
<div class="d-flex menu-click">
<nav class="scrollable menu-click">
<span *ngIf="userAuth" class="menu-click">
<strong class="menu-click">@ {{ userAuth.user.username }}</strong>
</span>
<a *ngIf="!userAuth" class="d-flex justify-content-center align-items-center nav-link m-0 menu-click" routerLink="/login" role="tab" (click)="onLinkClick('/login')">
<fa-icon class="menu-click" [icon]="['fas', 'user-circle']" [fixedWidth]="true" style="font-size: 25px;margin-right: 15px;"></fa-icon>
<span class="menu-click" style="font-size: 20px;">Sign in</span>
</a>
<ng-container *ngIf="userMenuGroups$ | async as menuGroups">
<div class="menu-click" *ngFor="let group of menuGroups" style="height: max-content;">
<h6 class="d-flex justify-content-between align-items-center mt-4 mb-2 text-uppercase menu-click">
<span class="menu-click">{{ group.title }}</span>
</h6>
<ul class="nav flex-column menu-click" *ngFor="let item of group.items" (click)="onLinkClick(item.link)">
<li class="nav-item d-flex justify-content-start align-items-center menu-click">
<fa-icon class="menu-click" [icon]="['fas', item.faIcon]" [fixedWidth]="true"></fa-icon>
<button *ngIf="item.link === 'logout'" class="btn nav-link menu-click" role="tab" (click)="logout()">{{ item.title }}</button>
<a *ngIf="item.title !== 'Logout'" class="nav-link menu-click" [routerLink]="[item.link]" role="tab">{{ item.title }}</a>
</li>
</ul>
</div>
</ng-container>
</nav>
</div>
</div>
@@ -0,0 +1,48 @@
.sidenav {
z-index: 1;
background-color: transparent;
width: 225px;
height: calc(100vh - 65px);
position: sticky;
top: 65px;
transition: 0.25s;
margin-left: -250px;
box-shadow: 5px 0px 30px 0px #000;
padding-bottom: 20px;
}
.scrollable {
overflow-x: hidden;
overflow-y: scroll;
}
.sidenav.open {
margin-left: 0px;
left: 0px;
display: block;
}
.sidenav a, button{
text-decoration: none;
color: lightgray;
margin-left: 20px;
}
.sidenav a:hover {
color: white;
}
.sidenav nav {
width: 100%;
height: calc(100vh - 65px);
background-color: #1d1f31;
padding-left: 20px;
padding-right: 20px;
padding-top: 20px;
padding-bottom: 20px;
@media (max-width: 991px) {
padding-bottom: 200px;
}
}
@media screen and (max-height: 450px) {
.sidenav a {font-size: 18px;}
}
@@ -0,0 +1,101 @@
import { Component, OnInit, Input, Output, EventEmitter, HostListener, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiService } from '../../services/api.service';
import { MenuGroup } from '../../interfaces/services.interface';
import { StorageService } from '../../services/storage.service';
import { Router, NavigationStart } from '@angular/router';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-menu',
templateUrl: './menu.component.html',
styleUrls: ['./menu.component.scss']
})
export class MenuComponent implements OnInit, OnDestroy {
@Input() navOpen: boolean = false;
@Output() loggedOut = new EventEmitter<boolean>();
@Output() menuToggled = new EventEmitter<boolean>();
userMenuGroups$: Observable<MenuGroup[]> | undefined;
userAuth: any | undefined;
isServicesPage = false;
constructor(
private apiService: ApiService,
private storageService: StorageService,
private router: Router,
private stateService: StateService
) {}
ngOnInit(): void {
this.userAuth = this.storageService.getAuth();
if (this.stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE) {
this.userMenuGroups$ = this.apiService.getUserMenuGroups$();
}
this.isServicesPage = this.router.url.includes('/services/');
this.router.events.subscribe((event) => {
if (event instanceof NavigationStart) {
if (!this.isServicesPage) {
this.toggleMenu(false);
}
}
});
}
toggleMenu(toggled: boolean) {
this.navOpen = toggled;
this.menuToggled.emit(toggled);
}
isSmallScreen() {
return window.innerWidth <= 767.98;
}
logout(): void {
this.apiService.logout$().subscribe(() => {
this.loggedOut.emit(true);
if (this.stateService.env.GIT_COMMIT_HASH_MEMPOOL_SPACE) {
this.userMenuGroups$ = this.apiService.getUserMenuGroups$();
this.router.navigateByUrl('/');
}
});
}
onLinkClick(link) {
if (!this.isServicesPage || this.isSmallScreen()) {
this.toggleMenu(false);
}
this.router.navigateByUrl(link);
}
hamburgerClick() {
this.toggleMenu(!this.navOpen);
this.stateService.menuOpen$.next(this.navOpen);
}
@HostListener('window:click', ['$event'])
onClick(event) {
const isServicesPageOnMobile = this.isServicesPage && this.isSmallScreen();
const cssClasses = event.target.className;
if (!cssClasses.indexOf) { // Click on chart or non html thingy, close the menu
if (!this.isServicesPage || isServicesPageOnMobile) {
this.toggleMenu(false);
}
return;
}
const isHamburger = cssClasses.indexOf('profile_image') !== -1;
const isMenu = cssClasses.indexOf('menu-click') !== -1;
if (!isHamburger && !isMenu && (!this.isServicesPage || isServicesPageOnMobile)) {
this.toggleMenu(false);
}
}
ngOnDestroy(): void {
this.stateService.menuOpen$.next(false);
}
}
@@ -18,6 +18,7 @@ export class MiningDashboardComponent implements OnInit, AfterViewInit {
private router: Router
) {
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`);
this.seoService.setDescription($localize`:@@meta.description.mining.dashboard:Get real-time Bitcoin mining stats like hashrate, difficulty adjustment, block rewards, pool dominance, and more.`);
}
ngOnInit(): void {
@@ -29,7 +30,7 @@ export class MiningDashboardComponent implements OnInit, AfterViewInit {
this.router.events.subscribe((e: NavigationStart) => {
if (e.type === EventType.NavigationStart) {
if (e.url.indexOf('graphs') === -1) { // The mining dashboard and the graph component are part of the same module so we can't use ngAfterViewInit in graphs.component.ts to blur the input
this.stateService.focusSearchInputDesktop();
this.stateService.focusSearchInputDesktop();
}
}
});
@@ -56,6 +56,7 @@ export class PoolRankingComponent implements OnInit {
this.miningWindowPreference = '1w';
} else {
this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.pool-ranking:See the top Bitcoin mining pools ranked by number of blocks mined, over your desired timeframe.`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
}
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
@@ -116,7 +117,7 @@ export class PoolRankingComponent implements OnInit {
} else if (this.widget) {
poolShareThreshold = 1;
}
const data: object[] = [];
let totalShareOther = 0;
let totalBlockOther = 0;
@@ -83,6 +83,7 @@ export class PoolPreviewComponent implements OnInit {
}
this.seoService.setTitle(poolStats.pool.name);
this.seoService.setDescription($localize`:@@meta.description.mining.pool:See mining pool stats for ${poolStats.pool.name}\: most recent mined blocks, hashrate over time, total block reward to date, known coinbase addresses, and more.`);
let regexes = '"';
for (const regex of poolStats.pool.regexes) {
regexes += regex + '", "';
@@ -83,6 +83,7 @@ export class PoolComponent implements OnInit {
}),
map((poolStats) => {
this.seoService.setTitle(poolStats.pool.name);
this.seoService.setDescription($localize`:@@meta.description.mining.pool:See mining pool stats for ${poolStats.pool.name}\: most recent mined blocks, hashrate over time, total block reward to date, known coinbase addresses, and more.`);
let regexes = '"';
for (const regex of poolStats.pool.regexes) {
regexes += regex + '", "';
@@ -1,5 +1,6 @@
import { Component } from '@angular/core';
import { Env, StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
@Component({
selector: 'app-privacy-policy',
@@ -11,5 +12,11 @@ export class PrivacyPolicyComponent {
constructor(
private stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.setTitle('Privacy Policy');
this.seoService.setDescription('Trusted third parties are security holes, as are trusted first parties...you should only trust your own self-hosted instance of The Mempool Open Source Project™.');
}
}
@@ -1,6 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { ApiService } from '../../services/api.service';
import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
@Component({
selector: 'app-push-transaction',
@@ -16,12 +19,17 @@ export class PushTransactionComponent implements OnInit {
constructor(
private formBuilder: UntypedFormBuilder,
private apiService: ApiService,
public stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.pushTxForm = this.formBuilder.group({
txHash: ['', Validators.required],
});
this.seoService.setTitle($localize`:@@meta.title.push-tx:Broadcast Transaction`);
this.seoService.setDescription($localize`:@@meta.description.push-tx:Broadcast a transaction to the ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} network using the transaction's hash.`);
}
postTx() {
@@ -1,5 +1,5 @@
<div [formGroup]="rateUnitForm" class="text-small text-center">
<select formControlName="rateUnits" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 100px;" (change)="changeUnits()">
<select formControlName="rateUnits" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" style="width: 95px;" (change)="changeUnits()">
<option *ngFor="let unit of units" [value]="unit.name">{{ unit.label }}</option>
</select>
</div>
@@ -6,6 +6,8 @@ import { WebsocketService } from '../../services/websocket.service';
import { RbfTree } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
@Component({
selector: 'app-rbf-list',
@@ -26,6 +28,7 @@ export class RbfList implements OnInit, OnDestroy {
private apiService: ApiService,
public stateService: StateService,
private websocketService: WebsocketService,
private seoService: SeoService,
) { }
ngOnInit(): void {
@@ -51,9 +54,12 @@ export class RbfList implements OnInit, OnDestroy {
this.isLoading = false;
})
);
this.seoService.setTitle($localize`:@@meta.title.rbf-list:RBF Replacements`);
this.seoService.setDescription($localize`:@@meta.description.rbf-list:See the most recent RBF replacements on the Bitcoin${seoDescriptionNetwork(this.stateService.network)} network, updated in real-time.`);
}
ngOnDestroy(): void {
this.websocketService.stopTrackRbf();
}
}
}
@@ -10,15 +10,25 @@
<div *ngIf="countdown > 0" class="warning-label">{{ eventName }} in {{ countdown | number }} block{{ countdown === 1 ? '' : 's' }}!</div>
<div class="blockchain-wrapper" [class.time-ltr]="timeLtr" [class.time-rtl]="!timeLtr">
<div class="blockchain-wrapper" [class.time-ltr]="timeLtr" [class.time-rtl]="!timeLtr" #blockchainWrapper>
<div id="blockchain-container" [dir]="timeLtr ? 'rtl' : 'ltr'" #blockchainContainer
[class.menu-open]="menuOpen"
[class.menu-closing]="menuSliding && !menuOpen"
(mousedown)="onMouseDown($event)"
(pointerdown)="onPointerDown($event)"
(touchmove)="onTouchMove($event)"
(dragstart)="onDragStart($event)"
(scroll)="onScroll($event)"
>
<app-blockchain [pageIndex]="pageIndex" [pages]="pages" [blocksPerPage]="blocksPerPage" [minScrollWidth]="minScrollWidth" [scrollableMempool]="true" (mempoolOffsetChange)="onMempoolOffsetChange($event)"></app-blockchain>
<app-blockchain
[containerWidth]="chainWidth"
[pageIndex]="pageIndex"
[pages]="pages"
[blocksPerPage]="blocksPerPage"
[minScrollWidth]="minScrollWidth"
[scrollableMempool]="true"
(mempoolOffsetChange)="onMempoolOffsetChange($event)"
></app-blockchain>
</div>
<div class="reset-scroll" [class.hidden]="pageIndex === 0" (click)="resetScroll()">
<fa-icon [icon]="['fas', 'circle-left']" [fixedWidth]="true"></fa-icon>
@@ -6,6 +6,20 @@
overflow-y: hidden;
scrollbar-width: none;
-ms-overflow-style: none;
width: calc(100% + 120px);
transform: translateX(0px);
transition: transform 0;
&.menu-open {
transform: translateX(-112.5px);
transition: transform 0.25s;
}
&.menu-closing {
transform: translateX(0px);
transition: transform 0.25s;
}
}
#blockchain-container::-webkit-scrollbar {
@@ -28,8 +28,10 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
lastMark: MarkBlockState;
markBlockSubscription: Subscription;
blockCounterSubscription: Subscription;
@ViewChild('blockchainWrapper', { static: true }) blockchainWrapper: ElementRef;
@ViewChild('blockchainContainer') blockchainContainer: ElementRef;
resetScrollSubscription: Subscription;
resetScrollSubscription: Subscription;
menuSubscription: Subscription;
isMobile: boolean = false;
isiOS: boolean = false;
@@ -49,6 +51,12 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
velocity: number = 0;
mempoolOffset: number = 0;
private resizeObserver: ResizeObserver;
chainWidth: number = window.innerWidth;
menuOpen: boolean = false;
menuSliding: boolean = false;
menuTimeout: number;
constructor(
private stateService: StateService,
) {
@@ -151,6 +159,13 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
this.stateService.resetScroll$.next(false);
}
});
this.menuSubscription = this.stateService.menuOpen$.subscribe((open) => {
if (this.menuOpen !== open) {
this.menuOpen = open;
this.applyMenuScroll(this.menuOpen);
}
});
}
onMempoolOffsetChange(offset): void {
@@ -171,9 +186,18 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
}
}
applyMenuScroll(opening: boolean): void {
this.menuSliding = true;
window.clearTimeout(this.menuTimeout);
this.menuTimeout = window.setTimeout(() => {
this.menuSliding = false;
}, 300);
}
@HostListener('window:resize', ['$event'])
onResize(): void {
this.isMobile = window.innerWidth <= 767.98;
this.chainWidth = window.innerWidth;
this.isMobile = this.chainWidth <= 767.98;
let firstVisibleBlock;
let offset;
if (this.blockchainContainer?.nativeElement != null) {
@@ -188,7 +212,7 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
});
}
this.blocksPerPage = Math.ceil(window.innerWidth / this.blockWidth);
this.blocksPerPage = Math.ceil(this.chainWidth / this.blockWidth);
this.pageWidth = this.blocksPerPage * this.blockWidth;
this.minScrollWidth = this.firstPageWidth + (this.pageWidth * 2);
@@ -295,7 +319,7 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
onScroll(e) {
const middlePage = this.pageIndex === 0 ? this.pages[0] : this.pages[1];
// compensate for css transform
const translation = (this.isMobile ? window.innerWidth * 0.95 : window.innerWidth * 0.5);
const translation = (this.isMobile ? this.chainWidth * 0.95 : this.chainWidth * 0.5);
const backThreshold = middlePage.offset + (this.pageWidth * 0.5) + translation;
const forwardThreshold = middlePage.offset - (this.pageWidth * 0.5) + translation;
const scrollLeft = this.getConvertedScrollOffset();
@@ -414,10 +438,10 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
blockInViewport(height: number): boolean {
const firstHeight = this.pages[0].height;
const translation = (this.isMobile ? window.innerWidth * 0.95 : window.innerWidth * 0.5);
const translation = (this.isMobile ? this.chainWidth * 0.95 : this.chainWidth * 0.5);
const firstX = this.pages[0].offset - this.getConvertedScrollOffset() + translation;
const xPos = firstX + ((firstHeight - height) * 155);
return xPos > -55 && xPos < (window.innerWidth - 100);
return xPos > -55 && xPos < (this.chainWidth - 100);
}
getConvertedScrollOffset(): number {
@@ -458,5 +482,6 @@ export class StartComponent implements OnInit, OnDestroy, DoCheck {
this.markBlockSubscription.unsubscribe();
this.blockCounterSubscription.unsubscribe();
this.resetScrollSubscription.unsubscribe();
this.menuSubscription.unsubscribe();
}
}
@@ -62,6 +62,7 @@ export class StatisticsComponent implements OnInit {
this.inverted = this.storageService.getValue('inverted-graph') === 'true';
this.setFeeLevelDropdownData();
this.seoService.setTitle($localize`:@@5d4f792f048fcaa6df5948575d7cb325c9393383:Graphs`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.mempool:See mempool size (in MvB) and transactions per second (in vB/s) visualized over time.`);
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.graphWindowPreference = this.storageService.getValue('graphWindowPreference') ? this.storageService.getValue('graphWindowPreference').trim() : '2h';
@@ -74,6 +74,16 @@
<path fill="#FFFFFF" d="M128 768h256v64H128v-64z m320-384H128v64h320v-64z m128 192V448L384 640l192 192V704h320V576H576z m-288-64H128v64h160v-64zM128 704h160v-64H128v64z m576 64h64v128c-1 18-7 33-19 45s-27 18-45 19H64c-35 0-64-29-64-64V192c0-35 29-64 64-64h192C256 57 313 0 384 0s128 57 128 128h192c35 0 64 29 64 64v320h-64V320H64v576h640V768zM128 256h512c0-35-29-64-64-64h-64c-35 0-64-29-64-64s-29-64-64-64-64 29-64 64-29 64-64 64h-64c-35 0-64 29-64 64z" />
</svg>
</ng-container>
<ng-container *ngSwitchCase="'hamburger'">
<svg [attr.width]="width" [attr.height]="height" viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
<path stroke-width="0.5" stroke-linecap="round" d="M0.5 2.5 H7 M0.5 5 H5.5 M0.5 7.5 H7"></path>
</svg>
</ng-container>
<ng-container *ngSwitchCase="'anon'">
<svg [attr.width]="width" [attr.height]="height" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 9.5v-2a3 3 0 116 0v2c0 1.11-.603 2.08-1.5 2.599v1.224a1 1 0 00.629.928l2.05.82A3.693 3.693 0 0118.5 18.5h-13c0-1.51.92-2.868 2.321-3.428l2.05-.82a1 1 0 00.629-.929v-1.224A2.999 2.999 0 019 9.5z"></path>
</svg>
</ng-container>
</ng-container>
<ng-template #bitcoinLogo let-color let-width="width" let-height="height" let-viewBox="viewBox">
@@ -37,6 +37,7 @@ export class TelevisionComponent implements OnInit, OnDestroy {
ngOnInit() {
this.seoService.setTitle($localize`:@@46ce8155c9ab953edeec97e8950b5a21e67d7c4e:TV view`);
this.seoService.setDescription($localize`:@@meta.description.tv:See Bitcoin blocks and mempool congestion in real-time in a simplified format perfect for a TV.`);
this.websocketService.want(['blocks', 'live-2h-chart', 'mempool-blocks']);
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {
@@ -1,5 +1,6 @@
import { Component } from '@angular/core';
import { Env, StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
@Component({
selector: 'app-terms-of-service',
@@ -10,5 +11,11 @@ export class TermsOfServiceComponent {
constructor(
private stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.setTitle('Terms of Service');
this.seoService.setDescription('Out of respect for the Bitcoin community, the mempool.space website is Bitcoin Only and does not display any advertising.');
}
}
@@ -1,5 +1,6 @@
import { Component } from '@angular/core';
import { Env, StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
@Component({
selector: 'app-trademark-policy',
@@ -11,5 +12,11 @@ export class TrademarkPolicyComponent {
constructor(
private stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.setTitle('Trademark Policy');
this.seoService.setDescription('An overview of the trademarks registered by Mempool Space K.K. and The Mempool Open Source Project™ and what we consider to be lawful usage of those trademarks.');
}
}
@@ -15,6 +15,7 @@ import { CacheService } from '../../services/cache.service';
import { OpenGraphService } from '../../services/opengraph.service';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { CpfpInfo } from '../../interfaces/node-api.interface';
import { LiquidUnblinding } from './liquid-ublinding';
@@ -87,6 +88,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
this.seoService.setTitle(
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.transaction:Get real-time status, addresses, fees, script info, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} transaction with txid {txid}.`);
this.resetTransaction();
return merge(
of(true),
@@ -6,6 +6,13 @@
<app-truncate [text]="rbfTransaction.txid" [lastChars]="12" [link]="['/tx/' | relativeUrl, rbfTransaction.txid]"></app-truncate>
</div>
<div *ngIf="acceleratorAvailable && accelerateCtaType === 'alert' && !tx?.status?.confirmed && !tx?.acceleration" class="alert alert-mempool alert-dismissible" role="alert">
<span><a class="link accelerator" (click)="onAccelerateClicked()">Accelerate</a> this transaction using Mempool Accelerator &trade;</span>
<button type="button" class="close" aria-label="Close" (click)="dismissAccelAlert()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<ng-container *ngIf="!rbfTransaction || rbfTransaction?.size || tx">
<h1 i18n="shared.transaction">Transaction</h1>
@@ -66,12 +73,22 @@
<div class="col-sm">
<ng-container *ngTemplateOutlet="feeTable"></ng-container>
</div>
</div>
</div>
</ng-template>
<!-- Accelerator -->
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary">
<div class="title mt-3" id="acceleratePreviewAnchor">
<h2>Accelerate</h2>
</div>
<div class="box">
<app-accelerate-preview [tx]="tx" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview>
</div>
</ng-container>
<ng-template #unconfirmedTemplate>
<div class="box">
@@ -92,16 +109,16 @@
</ng-template>
</ng-template>
<tr *ngIf="!replaced && !isCached">
<td class="td-width" i18n="transaction.eta|Transaction ETA">ETA</td>
<td class="td-width align-items-center align-middle" i18n="transaction.eta|Transaction ETA">ETA</td>
<td>
<ng-template [ngIf]="this.mempoolPosition?.block == null" [ngIfElse]="estimationTmpl">
<span class="skeleton-loader"></span>
</ng-template>
<ng-template #estimationTmpl>
<ng-template [ngIf]="this.mempoolPosition.block >= 7" [ngIfElse]="belowBlockLimit">
<span class="eta d-flex">
<span [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'etaDeepMempool d-flex justify-content-end align-items-center' : ''">
<span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span>
<span class="ml-2"></span><a *ngIf="stateService.env.OFFICIAL_MEMPOOL_SPACE && stateService.env.ACCELERATOR && stateService.network === ''" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn badge badge-primary accelerate ml-auto" i18n="transaction.accelerate|Accelerate button label">Accelerate</a>
<a *ngIf="acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerateDeepMempool" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
</span>
</ng-template>
<ng-template #belowBlockLimit>
@@ -109,9 +126,9 @@
<app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time>
</ng-template>
<ng-template #timeEstimateDefault>
<span class="d-flex">
<span class="eta justify-content-end" [class]="(acceleratorAvailable && accelerateCtaType === 'button') ? 'd-flex align-items-center' : ''">
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.timeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
<span class="ml-2"></span><a *ngIf="stateService.env.OFFICIAL_MEMPOOL_SPACE && stateService.env.ACCELERATOR && stateService.network === ''" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn badge badge-primary accelerate ml-auto" i18n="transaction.accelerate|Accelerate button label">Accelerate</a>
<a *ngIf="acceleratorAvailable && accelerateCtaType === 'button' && !tx?.acceleration" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn btn-sm accelerate" i18n="transaction.accelerate|Accelerate button label" (click)="onAccelerateClicked()">Accelerate</a>
</span>
</ng-template>
</ng-template>
@@ -130,7 +130,7 @@
}
.table {
tr td {
tr td {
padding: 0.75rem 0.5rem;
@media (min-width: 576px) {
padding: 0.75rem 0.75rem;
@@ -138,7 +138,7 @@
&:last-child {
text-align: right;
@media (min-width: 850px) {
text-align: left;
text-align: left;
}
}
.btn {
@@ -218,21 +218,52 @@
}
}
.link.accelerator {
cursor: pointer;
}
.eta {
display: flex;
justify-content: end;
flex-wrap: wrap;
align-content: center;
@media (min-width: 850px) {
justify-content: space-between;
justify-content: left !important;
}
}
.accelerate {
display: flex !important;
align-self: auto;
margin-top: 3px;
@media (min-width: 850px) {
justify-self: start;
margin-left: auto;
background-color: #653b9c;
@media (max-width: 849px) {
margin-left: 5px;
}
}
.etaDeepMempool {
display: flex !important;
justify-content: end;
flex-wrap: wrap;
align-content: center;
@media (max-width: 995px) {
justify-content: left !important;
}
@media (max-width: 849px) {
justify-content: right !important;
}
}
.accelerateDeepMempool {
align-self: auto;
margin-top: 3px;
margin-left: auto;
background-color: #653b9c;
@media (max-width: 995px) {
margin-left: 0px;
}
}
@media (max-width: 849px) {
margin-left: 5px;
}
}
@@ -19,6 +19,8 @@ import { WebsocketService } from '../../services/websocket.service';
import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { StorageService } from '../../services/storage.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment } from '../../interfaces/node-api.interface';
import { LiquidUnblinding } from './liquid-ublinding';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
@@ -88,6 +90,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
rbfEnabled: boolean;
taprootEnabled: boolean;
hasEffectiveFeeRate: boolean;
accelerateCtaType: 'alert' | 'button' = 'alert';
acceleratorAvailable: boolean = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
showAccelerationSummary = false;
scrollIntoAccelPreview = false;
@ViewChild('graphContainer')
graphContainer: ElementRef;
@@ -104,14 +110,22 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
private apiService: ApiService,
private seoService: SeoService,
private priceService: PriceService,
private storageService: StorageService
) {}
ngOnInit() {
this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
this.websocketService.want(['blocks', 'mempool-blocks']);
this.stateService.networkChanged$.subscribe(
(network) => (this.network = network)
(network) => {
this.network = network;
this.acceleratorAvailable = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
}
);
this.accelerateCtaType = (this.storageService.getValue('accel-cta-type') as 'alert' | 'button') ?? 'alert';
this.setFlowEnabled();
this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
this.hideFlow = !!hide;
@@ -161,34 +175,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
})
)
.subscribe((cpfpInfo) => {
if (!cpfpInfo || !this.tx) {
this.cpfpInfo = null;
this.hasEffectiveFeeRate = false;
return;
}
// merge ancestors/descendants
const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])];
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
relatives.push(cpfpInfo.bestDescendant);
}
const hasRelatives = !!relatives.length;
if (!cpfpInfo.effectiveFeePerVsize && hasRelatives) {
let totalWeight =
this.tx.weight +
relatives.reduce((prev, val) => prev + val.weight, 0);
let totalFees =
this.tx.fee +
relatives.reduce((prev, val) => prev + val.fee, 0);
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
} else {
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
}
if (cpfpInfo.acceleration) {
this.tx.acceleration = cpfpInfo.acceleration;
}
this.cpfpInfo = cpfpInfo;
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
this.setCpfpInfo(cpfpInfo);
});
this.fetchRbfSubscription = this.fetchRbfHistory$
@@ -259,6 +246,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
mempoolPosition: this.mempoolPosition
});
this.txInBlockIndex = this.mempoolPosition.block;
if (txPosition.cpfp !== undefined) {
this.setCpfpInfo(txPosition.cpfp);
}
}
} else {
this.mempoolPosition = null;
@@ -297,6 +288,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.seoService.setTitle(
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.transaction:Get real-time status, addresses, fees, script info, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} transaction with txid {txid}.`);
this.resetTransaction();
return merge(
of(true),
@@ -399,7 +391,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.blockConversion = price;
})
).subscribe();
setTimeout(() => { this.applyFragment(); }, 0);
},
(error) => {
@@ -486,6 +478,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.setGraphSize();
}
dismissAccelAlert(): void {
this.storageService.setValue('accel-cta-type', 'button');
this.accelerateCtaType = 'button';
}
onAccelerateClicked() {
if (!this.txId) {
return;
}
this.showAccelerationSummary = true && this.acceleratorAvailable;
this.scrollIntoAccelPreview = !this.scrollIntoAccelPreview;
return false;
}
handleLoadElectrsTransactionError(error: any): Observable<any> {
if (error.status === 404 && /^[a-fA-F0-9]{64}$/.test(this.txId)) {
this.websocketService.startMultiTrackTransaction(this.txId);
@@ -507,6 +513,37 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
setCpfpInfo(cpfpInfo: CpfpInfo): void {
if (!cpfpInfo || !this.tx) {
this.cpfpInfo = null;
this.hasEffectiveFeeRate = false;
return;
}
// merge ancestors/descendants
const relatives = [...(cpfpInfo.ancestors || []), ...(cpfpInfo.descendants || [])];
if (cpfpInfo.bestDescendant && !cpfpInfo.descendants?.length) {
relatives.push(cpfpInfo.bestDescendant);
}
const hasRelatives = !!relatives.length;
if (!cpfpInfo.effectiveFeePerVsize && hasRelatives) {
const totalWeight =
this.tx.weight +
relatives.reduce((prev, val) => prev + val.weight, 0);
const totalFees =
this.tx.fee +
relatives.reduce((prev, val) => prev + val.fee, 0);
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
} else {
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
}
if (cpfpInfo.acceleration) {
this.tx.acceleration = cpfpInfo.acceleration;
}
this.cpfpInfo = cpfpInfo;
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
}
setFeatures(): void {
if (this.tx) {
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
@@ -69,6 +69,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
ngOnInit(): void {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.seoService.resetTitle();
this.seoService.resetDescription();
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
this.websocketService.startTrackRbfSummary();
this.network$ = merge(of(''), this.stateService.networkChanged$);
@@ -155,7 +155,7 @@ ul.no-bull.block-audit code{
#doc-nav-desktop.fixed {
float: unset;
position: fixed;
top: 20px;
top: 80px;
overflow-y: auto;
height: calc(100vh - 50px);
scrollbar-color: #2d3348 #11131f;
@@ -43,7 +43,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
if (this.faqTemplates) {
this.faqTemplates.forEach((x) => this.dict[x.type] = x.template);
}
this.desktopDocsNavPosition = ( window.pageYOffset > 182 ) ? "fixed" : "relative";
this.desktopDocsNavPosition = ( window.pageYOffset > 115 ) ? "fixed" : "relative";
this.mobileViewport = window.innerWidth <= 992;
}
@@ -113,7 +113,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
}
onDocScroll() {
this.desktopDocsNavPosition = ( window.pageYOffset > 182 ) ? "fixed" : "relative";
this.desktopDocsNavPosition = ( window.pageYOffset > 115 ) ? "fixed" : "relative";
}
anchorLinkClick( event: any ) {
+34 -15
View File
@@ -28,21 +28,6 @@ export class DocsComponent implements OnInit {
ngOnInit(): void {
this.websocket.want(['blocks']);
const url = this.route.snapshot.url;
if (url[0].path === "faq" ) {
this.activeTab = 0;
this.seoService.setTitle($localize`:@@docs.faq.button-title:FAQ`);
} else if( url[1].path === "rest" ) {
this.activeTab = 1;
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
} else if( url[1].path === "websocket" ) {
this.activeTab = 2;
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
} else {
this.activeTab = 3;
this.seoService.setTitle($localize`:@@e351b40b3869a5c7d19c3d4918cb1ac7aaab95c4:API`);
}
this.env = this.stateService.env;
this.showWebSocketTab = ( ! ( ( this.stateService.network === "bisq" ) || ( this.stateService.network === "liquidtestnet" ) ) );
this.showFaqTab = ( this.env.BASE_MODULE === 'mempool' ) ? true : false;
@@ -51,6 +36,40 @@ export class DocsComponent implements OnInit {
document.querySelector<HTMLElement>( "html" ).style.scrollBehavior = "smooth";
}
ngDoCheck(): void {
const url = this.route.snapshot.url;
if (url[0].path === "faq" ) {
this.activeTab = 0;
this.seoService.setTitle($localize`:@@meta.title.docs.faq:FAQ`);
this.seoService.setDescription($localize`:@@meta.description.docs.faq:Get answers to common questions like: What is a mempool? Why isn't my transaction confirming? How can I run my own instance of The Mempool Open Source Project? And more.`);
} else if( url[1].path === "rest" ) {
this.activeTab = 1;
this.seoService.setTitle($localize`:@@meta.title.docs.rest:REST API`);
if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) {
this.seoService.setDescription($localize`:@@meta.description.docs.rest-liquid:Documentation for the liquid.network REST API service: get info on addresses, transactions, assets, blocks, and more.`);
} else if( this.stateService.network === 'bisq' ) {
this.seoService.setDescription($localize`:@@meta.description.docs.rest-bisq:Documentation for the bisq.markets REST API service: get info on recent trades, current offers, transactions, network state, and more.`);
} else {
this.seoService.setDescription($localize`:@@meta.description.docs.rest-bitcoin:Documentation for the mempool.space REST API service: get info on addresses, transactions, blocks, fees, mining, the Lightning network, and more.`);
}
} else if( url[1].path === "websocket" ) {
this.activeTab = 2;
this.seoService.setTitle($localize`:@@meta.title.docs.websocket:WebSocket API`);
if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) {
this.seoService.setDescription($localize`:@@meta.description.docs.websocket-liquid:Documentation for the liquid.network WebSocket API service: get real-time info on blocks, mempools, transactions, addresses, and more.`);
} else {
this.seoService.setDescription($localize`:@@meta.description.docs.websocket-bitcoin:Documentation for the mempool.space WebSocket API service: get real-time info on blocks, mempools, transactions, addresses, and more.`);
}
} else {
this.activeTab = 3;
this.seoService.setTitle($localize`:@@meta.title.docs.websocket:Electrum RPC`);
this.seoService.setDescription($localize`:@@meta.description.docs.electrumrpc:Documentation for our Electrum RPC interface: get instant, convenient, and reliable access to an Esplora instance.`);
}
}
ngOnDestroy(): void {
document.querySelector<HTMLElement>( "html" ).style.scrollBehavior = "auto";
}
@@ -19,7 +19,7 @@ export interface Transaction {
ancestors?: Ancestor[];
bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean;
acceleration?: number;
acceleration?: boolean;
deleteAfter?: number;
_unblinded?: any;
_deduced?: boolean;
@@ -27,7 +27,7 @@ export interface CpfpInfo {
effectiveFeePerVsize?: number;
sigops?: number;
adjustedVsize?: number;
acceleration?: number;
acceleration?: boolean;
}
export interface RbfInfo {
@@ -0,0 +1,13 @@
import { IconName } from '@fortawesome/fontawesome-common-types';
export type MenuItem = {
title: string;
i18n: string;
faIcon: IconName;
link: string;
};
export type MenuGroup = {
title: string;
i18n: string;
items: MenuItem[];
}
@@ -95,7 +95,7 @@ export interface TransactionStripped {
}
export interface IBackendInfo {
hostname: string;
hostname?: string;
gitCommit: string;
version: string;
}
@@ -34,6 +34,7 @@ export class ChannelPreviewComponent implements OnInit {
this.openGraphService.waitFor('channel-data-' + this.shortId);
this.error = null;
this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
this.seoService.setDescription($localize`:@@meta.description.lightning.channel:Overview for Lightning channel ${params.get('short_id')}. See channel capacity, the Lightning nodes involved, related on-chain transactions, and more.`);
return this.lightningApiService.getChannel$(params.get('short_id'))
.pipe(
tap((data) => {
@@ -35,6 +35,7 @@ export class ChannelComponent implements OnInit {
.pipe(
tap((value) => {
this.seoService.setTitle($localize`Channel: ${value.short_id}`);
this.seoService.setDescription($localize`:@@meta.description.lightning.channel:Overview for Lightning channel ${value.short_id}. See channel capacity, the Lightning nodes involved, related on-chain transactions, and more.`);
}),
catchError((err) => {
this.error = err;
@@ -31,6 +31,7 @@ export class GroupPreviewComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle(`Mempool.Space Lightning Nodes`);
this.seoService.setDescription(`See all Lightning nodes run by mempool.space -- these are the nodes that provide the data on the mempool.space Lightning dashboard.`);
this.nodes$ = this.activatedRoute.paramMap
.pipe(
@@ -39,6 +39,7 @@ export class GroupComponent implements OnInit {
});
this.seoService.setTitle(`Mempool.space Lightning Nodes`);
this.seoService.setDescription(`See all Lightning nodes run by mempool.space -- these are the nodes that provide the data on the mempool.space Lightning dashboard.`);
this.nodes$ = this.lightningApiService.getNodGroupNodes$('mempool.space')
.pipe(
@@ -25,6 +25,7 @@ export class LightningDashboardComponent implements OnInit, AfterViewInit {
ngOnInit(): void {
this.seoService.setTitle($localize`:@@142e923d3b04186ac6ba23387265d22a2fa404e0:Lightning Explorer`);
this.seoService.setDescription($localize`:@@meta.description.lightning.dashboard:Get stats on the Lightning network (aggregate capacity, connectivity, etc) and Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc).`);
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
@@ -49,6 +49,7 @@ export class NodePreviewComponent implements OnInit {
}),
map((node) => {
this.seoService.setTitle(`Node: ${node.alias}`);
this.seoService.setDescription($localize`:@@meta.description.lightning.node:Overview for the Lightning network node named ${node.alias}. See channels, capacity, location, fee stats, and more.`);
const socketsObject = [];
const socketTypesMap = {};
@@ -60,6 +60,7 @@ export class NodeComponent implements OnInit {
}),
map((node) => {
this.seoService.setTitle($localize`Node: ${node.alias}`);
this.seoService.setDescription($localize`:@@meta.description.lightning.node:Overview for the Lightning network node named ${node.alias}. See channels, capacity, location, fee stats, and more.`);
this.clearnetSocketCount = 0;
this.torSocketCount = 0;
@@ -26,7 +26,7 @@ export class NodesChannelsMap implements OnInit {
@Input() disableSpinner = false;
@Output() readyEvent = new EventEmitter();
channelsObservable: Observable<any>;
channelsObservable: Observable<any>;
center: number[] | undefined;
zoom: number | undefined;
@@ -41,7 +41,7 @@ export class NodesChannelsMap implements OnInit {
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'canvas',
};
};
constructor(
private seoService: SeoService,
@@ -64,15 +64,16 @@ export class NodesChannelsMap implements OnInit {
this.zoom = 1.4;
this.center = [0, 10];
}
if (this.style === 'graph') {
this.seoService.setTitle($localize`Lightning Nodes Channels World Map`);
this.seoService.setDescription($localize`:@@meta.description.lightning.node-map:See the channels of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details.`);
}
if (['nodepage', 'channelpage'].includes(this.style)) {
this.nodeSize = 8;
}
this.channelsObservable = this.activatedRoute.paramMap
.pipe(
delay(100),
@@ -81,7 +82,7 @@ export class NodesChannelsMap implements OnInit {
if (this.style === 'channelpage' && this.channel.length === 0 || !this.hasLocation) {
this.isLoading = false;
}
return zip(
this.assetsService.getWorldMapJson$,
this.style !== 'channelpage' ? this.apiService.getChannelsGeo$(params.get('public_key') ?? undefined, this.style) : [''],
@@ -140,7 +141,7 @@ export class NodesChannelsMap implements OnInit {
// on top of each other
let random = Math.random() * 2 * Math.PI;
let random2 = Math.random() * 0.01;
if (!nodesPubkeys[node1UniqueId]) {
nodes.push([
channel[node1GpsLat] + random2 * Math.cos(random),
@@ -167,7 +168,7 @@ export class NodesChannelsMap implements OnInit {
}
const channelLoc = [];
channelLoc.push(nodesPubkeys[node1UniqueId].slice(0, 2));
channelLoc.push(nodesPubkeys[node1UniqueId].slice(0, 2));
channelLoc.push(nodesPubkeys[node2UniqueId].slice(0, 2));
channelsLoc.push(channelLoc);
}
@@ -326,7 +327,7 @@ export class NodesChannelsMap implements OnInit {
this.chartInstance.on('finished', () => {
this.isLoading = false;
});
if (this.style === 'widget') {
this.chartInstance.getZr().on('click', (e) => {
this.zone.run(() => {
@@ -335,7 +336,7 @@ export class NodesChannelsMap implements OnInit {
});
});
}
this.chartInstance.on('click', (e) => {
if (e.data) {
this.zone.run(() => {
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff. Show more