Compare commits

..

138 Commits

Author SHA1 Message Date
Mononaut
fa914e7958 Fix missing menu content on dashboards 2023-11-22 18:51:43 +09:00
wiz
04eeb19bb9 Fix string in community sponsor CTA button box 2023-11-22 17:56:56 +09:00
wiz
671540af78 Merge pull request #4407 from mempool/mononaut/8chain
Standalone multi-block view page
2023-11-22 17:54:38 +09:00
Mononaut
1a732f18fc Remove font, update 8 block defaults, fix deprecated css 2023-11-22 17:47:51 +09:00
Mononaut
729fb3bb9d Move standalone blocks page to /view/blocks 2023-11-22 17:47:51 +09:00
Mononaut
fc56f273d4 Simplify 8chain block info 2023-11-22 17:47:51 +09:00
Mononaut
642be969a3 Add more block info, reduce font size 2023-11-22 17:47:50 +09:00
Mononaut
d69cdacd5e Add numBlocks param 2023-11-22 17:47:50 +09:00
Mononaut
ec8fc53dcb eight blocks 2023-11-22 17:47:49 +09:00
wiz
108d1762d6 Merge pull request #4404 from mempool/simon/truncate-text-link-fix
Fixes truncated links
2023-11-22 17:17:51 +09:00
wiz
879039ca8c Merge pull request #4406 from mempool/wiz/update-privacy-policy
Update our Privacy Policy for new backend
2023-11-22 17:16:38 +09:00
wiz
047d1463e7 Add missing <br> tag in privacy poilicy 2023-11-22 17:15:39 +09:00
wiz
8fc60fa086 Merge pull request #4405 from mempool/mononaut/simplify-acceleration-preview
simplify acceleration preview summary
2023-11-22 17:12:26 +09:00
wiz
bce68ee37f Merge branch 'master' into wiz/update-privacy-policy 2023-11-22 17:09:14 +09:00
wiz
2f25c128c1 Merge pull request #4395 from mempool/hunicus/about-sponsor-link
Change sponsor cta on about page
2023-11-22 17:06:55 +09:00
wiz
a76a600d3f Tweak community sponsor word 2023-11-22 17:06:02 +09:00
wiz
afdb419beb Update our Privacy Policy for new backend 2023-11-22 16:55:40 +09:00
Mononaut
8bd2aa3dd3 simplify acceleration preview summary 2023-11-22 16:38:58 +09:00
hunicus
a3f2c42b8e Replace vip events with early accelerator 2023-11-22 16:31:24 +09:00
softsimon
dbcd900056 Fixes truncated links 2023-11-22 16:22:41 +09:00
softsimon
5a6d6fae41 Merge pull request #4401 from mempool/mononaut/menu-footer-align-bug
Fix truncated hidden text layout flow bug
2023-11-22 15:35:26 +09:00
softsimon
6bb666ba5e Merge pull request #4402 from mempool/dependabot/npm_and_yarn/frontend/cypress-13.6.0
Bump cypress from 13.5.0 to 13.6.0 in /frontend
2023-11-22 14:47:49 +09:00
softsimon
505532f812 Merge pull request #4396 from mempool/mononaut/graph-tooltip-fixes
Fix mempool graph tooltip width & vb precision
2023-11-22 12:21:03 +09:00
dependabot[bot]
89eb02dad0 Bump cypress from 13.5.0 to 13.6.0 in /frontend
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.5.0 to 13.6.0.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v13.5.0...v13.6.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-22 02:21:44 +00:00
Mononaut
94a7b710c5 Fix truncated hidden text layout flow bug 2023-11-22 11:15:00 +09:00
softsimon
1e01f88c15 Merge pull request #4398 from mempool/mononaut/fix-left-margin
Fix negative left margin on master page
2023-11-21 20:41:38 +09:00
Mononaut
d471616a24 Fix negative left margin on master page 2023-11-21 20:29:41 +09:00
hunicus
9b8f70a0ae Change alignment and copy 2023-11-21 18:00:22 +09:00
Mononaut
ab079e9372 Fix mempool graph tooltip width & vb precision 2023-11-21 16:36:46 +09:00
hunicus
b77a16233b Change sponsor cta on about page 2023-11-21 14:56:53 +09:00
wiz
894075493b ops: Fix mempool backend config for liquid socket paths 2023-11-20 23:28:37 +09:00
wiz
954512cd8e Merge pull request #4303 from mempool/nymkappa/mega-branch
[Accelerator] Mega branch
2023-11-20 22:41:23 +09:00
nymkappa
deaf6ad6a5 [enterprise] fix api endpoints urls 2023-11-20 19:12:56 +09:00
nymkappa
98443b48ba Merge remote-tracking branch 'origin/nymkappa/mega-branch' into nymkappa/mega-branch 2023-11-20 17:16:44 +09:00
nymkappa
a7dc8793c2 [about page] un-hide sponsors buttons 2023-11-20 17:16:27 +09:00
wiz
84b8e5d472 Merge branch 'master' into nymkappa/mega-branch 2023-11-20 17:15:01 +09:00
softsimon
e768072799 Merge pull request #4388 from mempool/mononaut/rbf-cache-load-logs
More verbose RBF cache check logs
2023-11-19 17:34:00 +09:00
nymkappa
206706180f [accelerator] fix confirmation message when accelerating 2023-11-19 17:23:46 +09:00
nymkappa
0e3b3a0e00 [accelerator] fix message when trying to accelerate 2023-11-19 17:16:05 +09:00
wiz
28733c1e97 Merge branch 'master' into nymkappa/mega-branch 2023-11-19 16:07:03 +09:00
softsimon
7e74a26de2 Merge pull request #4257 from mempool/nymkappa/sticky-header-non-bitcoin
Sticky header non bitcoin
2023-11-19 15:27:52 +09:00
nymkappa
6d920e0ed3 [accelerator] hide accelerate banner 2023-11-18 18:36:17 +09:00
nymkappa
88e5f8a6af remove upgrade link when feature not available 2023-11-18 17:15:04 +09:00
nymkappa
ae82ac8368 Merge remote-tracking branch 'origin/nymkappa/mega-branch' into nymkappa/mega-branch 2023-11-18 15:21:01 +09:00
nymkappa
15cba58144 /image -> /images 2023-11-18 15:20:53 +09:00
wiz
2c820f1cc0 ops: Rewrite nginx config for new services API endpoints 2023-11-18 15:09:02 +09:00
wiz
4070492584 ops: Set nginx to ignore Pragma header for no-cache endpoints 2023-11-18 13:22:17 +09:00
wiz
82a43e25e0 ops: Set nginx no-cache headers for /api/v1/services/auth 2023-11-18 12:40:39 +09:00
wiz
5e45d8f3bc ops: Set nginx no-cache headers for /api/v1/services/account 2023-11-18 12:20:18 +09:00
wiz
e5709235f3 Merge branch 'master' into nymkappa/mega-branch 2023-11-18 11:30:35 +09:00
softsimon
59c513f2a5 Merge pull request #4389 from ncois/fix-p2pk-balance
Fix P2PK balance display in address page (duplicate)
2023-11-17 20:49:02 +09:00
ncois
76a07315f3 Fix P2PK balance display in address page 2023-11-17 09:52:48 +01:00
softsimon
5723f167df Merge pull request #4386 from mempool/mononaut/fix-electrum-p2pk
Fix electrum p2pk/scripthash format
2023-11-17 17:31:22 +09:00
Mononaut
6ac328c979 More verbose RBF cache check logs 2023-11-17 07:06:44 +00:00
softsimon
85e52d24c3 Merge pull request #4382 from shubhamkmr04/shubham/Change-ZEUS-logo
Change ZEUS Logo
2023-11-17 15:29:13 +09:00
Mononaut
7717c15666 Fix electrum p2pk/scripthash format 2023-11-17 03:49:31 +00:00
softsimon
61d7fd490a Merge pull request #4385 from mempool/mononaut/memory-usage-precision
Round memory usage to 3 significant figures
2023-11-17 11:48:44 +09:00
Mononaut
ccf952983b Round memory usage to 3sf 2023-11-17 02:22:57 +00:00
Shubham Kumar
602d7ce207 Merge branch 'master' into shubham/Change-ZEUS-logo 2023-11-16 12:42:19 +05:30
nymkappa
64a51803c4 Merge branch 'master' into nymkappa/mega-branch 2023-11-16 16:10:55 +09:00
shubham
a63e68e9e3 Change ZEUS Logo 2023-11-16 12:38:54 +05:30
softsimon
d4d17fa167 Merge pull request #4371 from mempool/nymkappa/outliers-toggle
[graph] add toggle to show/hide outliers in transaction vBytes per second graph
2023-11-16 14:59:50 +09:00
nymkappa
4cd8d70de5 cleanup if/else 2023-11-15 18:47:56 +09:00
nymkappa
dc26c6f105 [graph] don't change yaxis scale if no outliers - save state in localstorage 2023-11-15 18:46:33 +09:00
nymkappa
365954f5b4 Merge pull request #4304 from mempool/nymkappa/new-enterprise-launch
Prepare enterprise only launch
2023-11-15 18:24:48 +09:00
nymkappa
ff38073280 Merge branch 'nymkappa/mega-branch' into nymkappa/new-enterprise-launch 2023-11-15 18:22:37 +09:00
nymkappa
eeebfde33c Merge branch 'master' into nymkappa/mega-branch 2023-11-15 18:22:27 +09:00
softsimon
fa12233667 Merge pull request #4381 from mempool/mononaut/acc-preview-min-height
Minimum acceleration preview bar height
2023-11-15 18:22:05 +09:00
nymkappa
19477c4ee3 Merge branch 'master' into mononaut/acc-preview-min-height 2023-11-15 18:18:42 +09:00
nymkappa
e4b56bac88 Merge branch 'nymkappa/mega-branch' into nymkappa/new-enterprise-launch 2023-11-15 18:18:01 +09:00
nymkappa
d390fa8671 Merge branch 'master' into nymkappa/mega-branch 2023-11-15 18:17:48 +09:00
softsimon
f9f9c62608 Merge pull request #4374 from mempool/nymkappa/fix-hybrid-build-routing
[frontend] export MasterPageComponent for re-use in hybrid build
2023-11-15 18:02:25 +09:00
Mononaut
d9966143c1 Minimum acceleration preview bar height 2023-11-15 08:49:22 +00:00
nymkappa
57ab82ae7a Merge branch 'nymkappa/mega-branch' into nymkappa/new-enterprise-launch 2023-11-15 17:23:42 +09:00
nymkappa
756f6d8abe [frontend] export MasterPageComponent for re-use in hybrid build 2023-11-15 17:09:54 +09:00
softsimon
f0840a51d9 Merge pull request #4379 from mempool/mononaut/reduce-batch-sizes
Configurable mempool/electrs batch sizes
2023-11-15 16:56:35 +09:00
softsimon
eac8f8c2c6 Merge pull request #4368 from mempool/simon/truncate-selection
Make search and select work for truncated text
2023-11-15 16:25:17 +09:00
Mononaut
20f61fc6a0 Round batch sizes up 2023-11-15 06:58:00 +00:00
Mononaut
6454892d48 Use mempool/txs max_txs parameter 2023-11-15 06:57:31 +00:00
Mononaut
35d7c55c1d Configurable esplora batch sizes 2023-11-15 06:12:15 +00:00
softsimon
86fe6a802b Fixing mobile overflow 2023-11-15 15:07:14 +09:00
softsimon
d4568b631d Make search and select work for truncated text
fixes #4367
2023-11-15 14:39:23 +09:00
nymkappa
2d30c0b588 [graph] use echart echart yaxis max property instead of modifying the data itself 2023-11-15 14:08:44 +09:00
wiz
1aea3fcac5 Merge pull request #4378 from mempool/mononaut/tomahawk-logs-fix
Set fallback server out-of-sync when unreachable
2023-11-14 19:27:13 +09:00
Mononaut
5cfd599018 Set fallback server out-of-symc when unreachable 2023-11-14 10:17:02 +00:00
wiz
d8f2462ff0 Merge pull request #4377 from mempool/mononaut/tomahawk-logs
🥩🙂🪵📝
2023-11-14 19:10:41 +09:00
Mononaut
85091e1f3a 🥩🙂🪵📝 2023-11-14 09:55:02 +00:00
softsimon
3a8d19062f Merge pull request #4271 from mempool/mononaut/refactor-task-scheduler
Refactor indexer scheduling to avoid accumulating identical tasks
2023-11-14 18:13:48 +09:00
wiz
6913946079 ops: Upgrade mariadb to 10.11.x 2023-11-14 17:46:52 +09:00
wiz
fdd18317f9 ops: Kill node before sh in stop script 2023-11-14 17:46:42 +09:00
softsimon
a7c64c0df3 Merge branch 'master' into mononaut/refactor-task-scheduler 2023-11-14 17:17:55 +09:00
softsimon
f2fb2f98f1 Merge pull request #4375 from mempool/mononaut/db-error-log-levels
Support different log levels for database query error messages
2023-11-14 17:17:31 +09:00
Mononaut
7aad664112 Support different log levels for database query error messages 2023-11-14 07:59:24 +00:00
nymkappa
fa040ca19f [frontend] export MasterPageComponent for re-use in hybrid build 2023-11-14 15:53:37 +09:00
Mononaut
00887bc24b Refactor indexer scheduling to avoid accumulating identical tasks 2023-11-14 06:30:22 +00:00
softsimon
ab8b557e73 Merge pull request #4373 from mempool/mononaut/fix-silent-db-exceptions
Fix silently unhandled database exceptions
2023-11-14 15:27:52 +09:00
Mononaut
5c0a59d2f6 handle unknown query types in db error log 2023-11-14 06:13:06 +00:00
Mononaut
29cbdf6cd5 handle null query in error log 2023-11-14 05:52:27 +00:00
Mononaut
08b68ef8ba handle exception in db transaction rollback 2023-11-14 05:33:48 +00:00
Mononaut
1ae34e069c Fix silently unhandled database exceptions 2023-11-14 05:11:51 +00:00
softsimon
5bad829afc Merge pull request #4316 from starius/taproot-channels
[lightning] add taproot-channels to node features
2023-11-14 13:54:31 +09:00
softsimon
562cd5683a Merge branch 'master' into taproot-channels 2023-11-14 13:41:44 +09:00
nymkappa
cbf2395009 Merge branch 'master' into nymkappa/mega-branch 2023-11-14 11:12:11 +09:00
nymkappa
c393483590 [graph] add toggle to show/hide outliers in transaction vBytes per second graph 2023-11-14 10:51:51 +09:00
wiz
cbe1ec4e72 Merge pull request #4356 from mempool/mononaut/fix-pid-lock
Recover from stale PID file
2023-11-13 22:19:43 +09:00
Mononaut
c6a92083a8 Fix pid parsing on release lock 2023-11-13 07:54:11 +00:00
Mononaut
8c4b488251 handle SIGHUP exit code 2023-11-13 07:54:11 +00:00
Mononaut
3639dcc92a Recover from stale PID file 2023-11-13 07:54:11 +00:00
nymkappa
a9a3623539 Merge pull request #4341 from mempool/nymkappa/hide-accelerate-cta-loading
[accelerator] hide cta while the tx page is loading
2023-11-02 10:21:46 +09:00
nymkappa
27966ad8ec [accelerator] hide cta while the tx page is loading 2023-11-02 10:21:15 +09:00
nymkappa
5d1ebc6d31 Merge pull request #4340 from mempool/nymkappa/fix-search-align-issue
[CSS] fix search align issue when hamburger icon is showing
2023-11-01 17:49:26 +09:00
nymkappa
a68c2a2be6 [css] fix search bar align issue when hamburger icon is showing 2023-11-01 17:47:20 +09:00
Boris Nagaev
2bc3352785 [lightning] add taproot-channels to node features
Update from https://github.com/lightningnetwork/lnd/blob/master/lnwire/features.go
2023-10-20 04:49:14 +02:00
Boris Nagaev
50460d4025 contributer license 2023-10-20 04:48:34 +02:00
nymkappa
7d3f82eca0 [footer] remove "Sign-up" label 2023-10-05 11:16:01 +02:00
nymkappa
20026f974f Merge remote-tracking branch 'origin/nymkappa/accel-button' into nymkappa/mega-branch 2023-10-04 12:11:10 +02:00
nymkappa
3d964fcdfa Merge branch 'nymkappa/about-page-profile-image' into nymkappa/mega-branch 2023-10-04 12:11:05 +02:00
nymkappa
9e8b2957d0 Merge branch 'nymkappa/menu' into nymkappa/mega-branch 2023-10-04 12:10:58 +02:00
nymkappa
c04e92a686 Merge branch 'nymkappa/accelerate-preview-login-cta' into nymkappa/mega-branch 2023-10-04 12:10:46 +02:00
nymkappa
2dd6dd9233 Merge branch 'nymkappa/polish-accelerate-cta' into nymkappa/mega-branch 2023-10-04 12:10:39 +02:00
nymkappa
e9b72776ed Merge remote-tracking branch 'origin/nymkappa/fix-menu-scroll' into nymkappa/mega-branch 2023-10-04 12:10:28 +02:00
nymkappa
23df1f012c Merge remote-tracking branch 'origin/nymkappa/sticky-header-non-bitcoin' into nymkappa/mega-branch 2023-10-04 12:10:17 +02:00
nymkappa
14a41b3108 [bisq] sticky header 2023-09-15 16:05:10 +02:00
nymkappa
81d1a809d2 [liquid] sticky header 2023-09-15 16:02:39 +02:00
nymkappa
a000438277 [UI] menu username ellipsis 2023-09-13 09:30:11 +02:00
nymkappa
e3fffd8fb2 [UI] fix menu scroll issue 2023-09-13 09:21:31 +02:00
nymkappa
5dfd1a495e Merge branch 'master' into nymkappa/about-page-profile-image 2023-09-12 14:34:34 +02:00
nymkappa
a463fc289f Merge branch 'master' into nymkappa/menu 2023-09-12 14:34:28 +02:00
nymkappa
a8e6d9b4b9 Merge branch 'master' into nymkappa/accelerate-preview-login-cta 2023-09-12 14:34:22 +02:00
nymkappa
9ed7e80c44 fix typo post merge 2023-09-12 14:15:29 +02:00
nymkappa
596d55e413 Merge branch 'master' into nymkappa/accel-button 2023-09-12 14:05:56 +02:00
nymkappa
0e1a9d8619 [accelerator] blinking cta, polish accel preview 2023-09-03 16:05:12 +03:00
nymkappa
d09668aaa6 [accelerator] login CTA with redirection 2023-09-03 12:20:30 +03:00
wiz
93aa08b60d Merge branch 'master' into nymkappa/menu 2023-08-31 02:02:50 +09:00
nymkappa
4664e55513 [footer] fix positioning when menu component is not loaded 2023-08-30 18:58:21 +02:00
nymkappa
6199216c54 use new services api to fetch chads profile image as well 2023-08-10 18:08:30 +09:00
nymkappa
b988a4c526 use new services api to fetch chad/whale profile image 2023-08-10 18:01:05 +09:00
nymkappa
89be841e64 [accelerator] hide accelerate button if already accelerating 2023-08-08 17:10:59 +09:00
80 changed files with 1186 additions and 379 deletions

View File

@@ -52,6 +52,7 @@
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:3000",
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet",
"BATCH_QUERY_BASE_SIZE": 1000,
"RETRY_UNIX_SOCKET_AFTER": 30000,
"REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000,
@@ -132,6 +133,11 @@
"BISQ_URL": "https://bisq.markets/api",
"BISQ_ONION": "http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api"
},
"REDIS": {
"ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
"BATCH_QUERY_BASE_SIZE": 5000
},
"REPLICATION": {
"ENABLED": false,
"AUDIT": false,

View File

@@ -53,6 +53,7 @@
"ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"BATCH_QUERY_BASE_SIZE": 1000,
"RETRY_UNIX_SOCKET_AFTER": 888,
"REQUEST_TIMEOUT": 10000,
"FALLBACK_TIMEOUT": 5000,
@@ -140,6 +141,7 @@
},
"REDIS": {
"ENABLED": false,
"UNIX_SOCKET_PATH": "/tmp/redis.sock"
"UNIX_SOCKET_PATH": "/tmp/redis.sock",
"BATCH_QUERY_BASE_SIZE": 5000
}
}

View File

@@ -55,6 +55,7 @@ describe('Mempool Backend Config', () => {
expect(config.ESPLORA).toStrictEqual({
REST_API_URL: 'http://127.0.0.1:3000',
UNIX_SOCKET_PATH: null,
BATCH_QUERY_BASE_SIZE: 1000,
RETRY_UNIX_SOCKET_AFTER: 30000,
REQUEST_TIMEOUT: 10000,
FALLBACK_TIMEOUT: 5000,
@@ -144,7 +145,8 @@ describe('Mempool Backend Config', () => {
expect(config.REDIS).toStrictEqual({
ENABLED: false,
UNIX_SOCKET_PATH: ''
UNIX_SOCKET_PATH: '',
BATCH_QUERY_BASE_SIZE: 5000,
});
});
});

View File

@@ -5,7 +5,7 @@ export interface AbstractBitcoinApi {
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
$getRawTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
$getMempoolTransactions(txids: string[]): Promise<IEsploraApi.Transaction[]>;
$getAllMempoolTransactions(lastTxid: string);
$getAllMempoolTransactions(lastTxid?: string, max_txs?: number);
$getTransactionHex(txId: string): Promise<string>;
$getBlockHeightTip(): Promise<number>;
$getBlockHashTip(): Promise<string>;

View File

@@ -77,7 +77,7 @@ class BitcoinApi implements AbstractBitcoinApi {
throw new Error('Method getMempoolTransactions not supported by the Bitcoin RPC API.');
}
$getAllMempoolTransactions(lastTxid: string): Promise<IEsploraApi.Transaction[]> {
$getAllMempoolTransactions(lastTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> {
throw new Error('Method getAllMempoolTransactions not supported by the Bitcoin RPC API.');
}

View File

@@ -573,7 +573,9 @@ class BitcoinRoutes {
}
try {
const addressData = await bitcoinApi.$getScriptHash(req.params.scripthash);
// electrum expects scripthashes in little-endian
const electrumScripthash = req.params.scripthash.match(/../g)?.reverse().join('') ?? '';
const addressData = await bitcoinApi.$getScriptHash(electrumScripthash);
res.json(addressData);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
@@ -590,11 +592,13 @@ class BitcoinRoutes {
}
try {
// electrum expects scripthashes in little-endian
const electrumScripthash = req.params.scripthash.match(/../g)?.reverse().join('') ?? '';
let lastTxId: string = '';
if (req.query.after_txid && typeof req.query.after_txid === 'string') {
lastTxId = req.query.after_txid;
}
const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.scripthash, lastTxId);
const transactions = await bitcoinApi.$getScriptHashTransactions(electrumScripthash, lastTxId);
res.json(transactions);
} catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {

View File

@@ -8,8 +8,9 @@ import logger from '../../logger';
interface FailoverHost {
host: string,
rtts: number[],
rtt: number
rtt: number,
failures: number,
latestHeight?: number,
socket?: boolean,
outOfSync?: boolean,
unreachable?: boolean,
@@ -92,6 +93,7 @@ class FailoverRouter {
host.rtts.unshift(rtt);
host.rtts.slice(0, 5);
host.rtt = host.rtts.reduce((acc, l) => acc + l, 0) / host.rtts.length;
host.latestHeight = height;
if (height == null || isNaN(height) || (maxHeight - height > 2)) {
host.outOfSync = true;
} else {
@@ -99,22 +101,23 @@ class FailoverRouter {
}
host.unreachable = false;
} else {
host.outOfSync = true;
host.unreachable = true;
}
}
this.sortHosts();
logger.debug(`Tomahawk ranking: ${this.hosts.map(host => '\navg rtt ' + Math.round(host.rtt).toString().padStart(5, ' ') + ' | reachable? ' + (!host.unreachable || false).toString().padStart(5, ' ') + ' | in sync? ' + (!host.outOfSync || false).toString().padStart(5, ' ') + ` | ${host.host}`).join('')}`);
logger.debug(`Tomahawk ranking:\n${this.hosts.map((host, index) => this.formatRanking(index, host, this.activeHost, maxHeight)).join('\n')}`);
// switch if the current host is out of sync or significantly slower than the next best alternative
if (this.activeHost.outOfSync || this.activeHost.unreachable || (this.activeHost !== this.hosts[0] && this.hosts[0].preferred) || (!this.activeHost.preferred && this.activeHost.rtt > (this.hosts[0].rtt * 2) + 50)) {
if (this.activeHost.unreachable) {
logger.warn(`Unable to reach ${this.activeHost.host}, failing over to next best alternative`);
logger.warn(`🚨🚨🚨 Unable to reach ${this.activeHost.host}, failing over to next best alternative 🚨🚨🚨`);
} else if (this.activeHost.outOfSync) {
logger.warn(`${this.activeHost.host} has fallen behind, failing over to next best alternative`);
logger.warn(`🚨🚨🚨 ${this.activeHost.host} has fallen behind, failing over to next best alternative 🚨🚨🚨`);
} else {
logger.debug(`${this.activeHost.host} is no longer the best esplora host`);
logger.debug(`🛠️ ${this.activeHost.host} is no longer the best esplora host 🛠️`);
}
this.electHost();
}
@@ -122,6 +125,11 @@ class FailoverRouter {
this.pollTimer = setTimeout(() => { this.pollHosts(); }, this.pollInterval);
}
private formatRanking(index: number, host: FailoverHost, active: FailoverHost, maxHeight: number): string {
const heightStatus = host.outOfSync ? '🚫' : (host.latestHeight && host.latestHeight < maxHeight ? '🟧' : '✅');
return `${host === active ? '⭐️' : ' '} ${host.rtt < Infinity ? Math.round(host.rtt).toString().padStart(5, ' ') + 'ms' : ' - '} ${host.unreachable ? '🔥' : '✅'} | block: ${host.latestHeight || '??????'} ${heightStatus} | ${host.host} ${host === active ? '⭐️' : ' '}`;
}
// sort hosts by connection quality, and update default fallback
private sortHosts(): void {
// sort by connection quality
@@ -156,7 +164,7 @@ class FailoverRouter {
private addFailure(host: FailoverHost): FailoverHost {
host.failures++;
if (host.failures > 5 && this.multihost) {
logger.warn(`Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative`);
logger.warn(`🚨🚨🚨 Too many esplora failures on ${this.activeHost.host}, falling back to next best alternative 🚨🚨🚨`);
this.electHost();
return this.activeHost;
} else {
@@ -225,8 +233,8 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.failoverRouter.$post<IEsploraApi.Transaction[]>('/internal/mempool/txs', txids, 'json');
}
async $getAllMempoolTransactions(lastSeenTxid?: string): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''));
async $getAllMempoolTransactions(lastSeenTxid?: string, max_txs?: number): Promise<IEsploraApi.Transaction[]> {
return this.failoverRouter.$get<IEsploraApi.Transaction[]>('/internal/mempool/txs' + (lastSeenTxid ? '/' + lastSeenTxid : ''), 'json', max_txs ? { max_txs } : null);
}
$getTransactionHex(txId: string): Promise<string> {

View File

@@ -761,8 +761,13 @@ class Blocks {
this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
if (!fastForwarded) {
const lastestPriceId = await PricesRepository.$getLatestPriceId();
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
let lastestPriceId;
try {
lastestPriceId = await PricesRepository.$getLatestPriceId();
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
} catch (e) {
logger.debug('failed to fetch latest price id from db: ' + (e instanceof Error ? e.message : e));
}
if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
await blocksRepository.$saveBlockPrices([{
height: blockExtended.height,
@@ -771,9 +776,7 @@ class Blocks {
this.updateTimerProgress(timer, `saved prices for ${this.currentBlockHeight}`);
} else {
logger.debug(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining);
setTimeout(() => {
indexer.runSingleTask('blocksPrices');
}, 10000);
indexer.scheduleSingleTask('blocksPrices', 10000);
}
// Save blocks summary for visualization if it's enabled

View File

@@ -44,9 +44,13 @@ export enum FeatureBits {
KeysendOptional = 55,
ScriptEnforcedLeaseRequired = 2022,
ScriptEnforcedLeaseOptional = 2023,
SimpleTaprootChannelsRequiredFinal = 80,
SimpleTaprootChannelsOptionalFinal = 81,
SimpleTaprootChannelsRequiredStaging = 180,
SimpleTaprootChannelsOptionalStaging = 181,
MaxBolt11Feature = 5114,
};
export const FeaturesMap = new Map<FeatureBits, string>([
[FeatureBits.DataLossProtectRequired, 'data-loss-protect'],
[FeatureBits.DataLossProtectOptional, 'data-loss-protect'],
@@ -85,6 +89,10 @@ export const FeaturesMap = new Map<FeatureBits, string>([
[FeatureBits.ZeroConfOptional, 'zero-conf'],
[FeatureBits.ShutdownAnySegwitRequired, 'shutdown-any-segwit'],
[FeatureBits.ShutdownAnySegwitOptional, 'shutdown-any-segwit'],
[FeatureBits.SimpleTaprootChannelsRequiredFinal, 'taproot-channels'],
[FeatureBits.SimpleTaprootChannelsOptionalFinal, 'taproot-channels'],
[FeatureBits.SimpleTaprootChannelsRequiredStaging, 'taproot-channels-staging'],
[FeatureBits.SimpleTaprootChannelsOptionalStaging, 'taproot-channels-staging'],
]);
/**

View File

@@ -126,7 +126,7 @@ class Mempool {
loadingIndicators.setProgress('mempool', count / expectedCount * 100);
while (!done) {
try {
const result = await bitcoinApi.$getAllMempoolTransactions(last_txid);
const result = await bitcoinApi.$getAllMempoolTransactions(last_txid, config.ESPLORA.BATCH_QUERY_BASE_SIZE);
if (result) {
for (const tx of result) {
const extendedTransaction = transactionUtils.extendMempoolTransaction(tx);
@@ -235,7 +235,7 @@ class Mempool {
if (!loaded) {
const remainingTxids = transactions.filter(txid => !this.mempoolCache[txid]);
const sliceLength = 10000;
const sliceLength = config.ESPLORA.BATCH_QUERY_BASE_SIZE;
for (let i = 0; i < Math.ceil(remainingTxids.length / sliceLength); i++) {
const slice = remainingTxids.slice(i * sliceLength, (i + 1) * sliceLength);
const txs = await transactionUtils.$getMempoolTransactionsExtended(slice, false, false, false);

View File

@@ -480,14 +480,15 @@ class RbfCache {
};
if (config.MEMPOOL.BACKEND === 'esplora') {
const sliceLength = 250;
let processedCount = 0;
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 40);
for (let i = 0; i < Math.ceil(txids.length / sliceLength); i++) {
const slice = txids.slice(i * sliceLength, (i + 1) * sliceLength);
processedCount += slice.length;
try {
const txs = await bitcoinApi.$getRawTransactions(slice);
logger.debug(`fetched ${slice.length} cached rbf transactions`);
processTxs(txs);
logger.debug(`processed ${slice.length} cached rbf transactions`);
logger.debug(`fetched and processed ${processedCount} of ${txids.length} cached rbf transactions (${(processedCount / txids.length * 100).toFixed(2)}%)`);
} catch (err) {
logger.err(`failed to fetch or process ${slice.length} cached rbf transactions`);
}

View File

@@ -122,8 +122,9 @@ class RedisCache {
async $removeTransactions(transactions: string[]) {
try {
await this.$ensureConnected();
for (let i = 0; i < Math.ceil(transactions.length / 10000); i++) {
const slice = transactions.slice(i * 10000, (i + 1) * 10000);
const sliceLength = config.REDIS.BATCH_QUERY_BASE_SIZE;
for (let i = 0; i < Math.ceil(transactions.length / sliceLength); i++) {
const slice = transactions.slice(i * sliceLength, (i + 1) * sliceLength);
await this.client.unlink(slice.map(txid => `mempool:tx:${txid}`));
logger.debug(`Deleted ${slice.length} transactions from the Redis cache`);
}

View File

@@ -43,6 +43,7 @@ interface IConfig {
ESPLORA: {
REST_API_URL: string;
UNIX_SOCKET_PATH: string | void | null;
BATCH_QUERY_BASE_SIZE: number;
RETRY_UNIX_SOCKET_AFTER: number;
REQUEST_TIMEOUT: number;
FALLBACK_TIMEOUT: number;
@@ -151,6 +152,7 @@ interface IConfig {
REDIS: {
ENABLED: boolean;
UNIX_SOCKET_PATH: string;
BATCH_QUERY_BASE_SIZE: number;
},
}
@@ -195,6 +197,7 @@ const defaults: IConfig = {
'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000',
'UNIX_SOCKET_PATH': null,
'BATCH_QUERY_BASE_SIZE': 1000,
'RETRY_UNIX_SOCKET_AFTER': 30000,
'REQUEST_TIMEOUT': 10000,
'FALLBACK_TIMEOUT': 5000,
@@ -303,6 +306,7 @@ const defaults: IConfig = {
'REDIS': {
'ENABLED': false,
'UNIX_SOCKET_PATH': '',
'BATCH_QUERY_BASE_SIZE': 5000,
},
};

View File

@@ -2,8 +2,10 @@ import * as fs from 'fs';
import path from 'path';
import config from './config';
import { createPool, Pool, PoolConnection } from 'mysql2/promise';
import { LogLevel } from './logger';
import logger from './logger';
import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } from 'mysql2/typings/mysql';
import { execSync } from 'child_process';
class DB {
constructor() {
@@ -32,7 +34,7 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
}
public async query<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
OkPacket[] | ResultSetHeader>(query, params?, connection?: PoolConnection): Promise<[T, FieldPacket[]]>
OkPacket[] | ResultSetHeader>(query, params?, errorLogLevel: LogLevel | 'silent' = 'debug', connection?: PoolConnection): Promise<[T, FieldPacket[]]>
{
this.checkDBFlag();
let hardTimeout;
@@ -54,19 +56,38 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
}).then(result => {
resolve(result);
}).catch(error => {
if (errorLogLevel !== 'silent') {
logger[errorLogLevel](`database query "${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}" failed!`);
}
reject(error);
}).finally(() => {
clearTimeout(timer);
});
});
} else {
const pool = await this.getPool();
return pool.query(query, params);
try {
const pool = await this.getPool();
return pool.query(query, params);
} catch (e) {
if (errorLogLevel !== 'silent') {
logger[errorLogLevel](`database query "${query?.sql?.slice(0, 160) || (typeof(query) === 'string' || query instanceof String ? query?.slice(0, 160) : 'unknown query')}" failed!`);
}
throw e;
}
}
}
private async $rollbackAtomic(connection: PoolConnection): Promise<void> {
try {
await connection.rollback();
await connection.release();
} catch (e) {
logger.warn('Failed to rollback incomplete db transaction: ' + (e instanceof Error ? e.message : e));
}
}
public async $atomicQuery<T extends RowDataPacket[][] | RowDataPacket[] | OkPacket |
OkPacket[] | ResultSetHeader>(queries: { query, params }[]): Promise<[T, FieldPacket[]][]>
OkPacket[] | ResultSetHeader>(queries: { query, params }[], errorLogLevel: LogLevel | 'silent' = 'debug'): Promise<[T, FieldPacket[]][]>
{
const pool = await this.getPool();
const connection = await pool.getConnection();
@@ -75,7 +96,7 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
const results: [T, FieldPacket[]][] = [];
for (const query of queries) {
const result = await this.query(query.query, query.params, connection) as [T, FieldPacket[]];
const result = await this.query(query.query, query.params, errorLogLevel, connection) as [T, FieldPacket[]];
results.push(result);
}
@@ -83,9 +104,8 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
return results;
} catch (e) {
logger.err('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e));
connection.rollback();
connection.release();
logger.warn('Could not complete db transaction, rolling back: ' + (e instanceof Error ? e.message : e));
this.$rollbackAtomic(connection);
throw e;
} finally {
connection.release();
@@ -105,26 +125,43 @@ import { FieldPacket, OkPacket, PoolOptions, ResultSetHeader, RowDataPacket } fr
public getPidLock(): boolean {
const filePath = path.join(config.DATABASE.PID_DIR || __dirname, `/mempool-${config.DATABASE.DATABASE}.pid`);
this.enforcePidLock(filePath);
fs.writeFileSync(filePath, `${process.pid}`);
return true;
}
private enforcePidLock(filePath: string): void {
if (fs.existsSync(filePath)) {
const pid = fs.readFileSync(filePath).toString();
if (pid !== `${process.pid}`) {
const msg = `Already running on PID ${pid} (or pid file '${filePath}' is stale)`;
const pid = parseInt(fs.readFileSync(filePath, 'utf-8'));
if (pid === process.pid) {
logger.warn('PID file already exists for this process');
return;
}
let cmd;
try {
cmd = execSync(`ps -p ${pid} -o args=`);
} catch (e) {
logger.warn(`Stale PID file at ${filePath}, but no process running on that PID ${pid}`);
return;
}
if (cmd && cmd.toString()?.includes('node')) {
const msg = `Another mempool nodejs process is already running on PID ${pid}`;
logger.err(msg);
throw new Error(msg);
} else {
return true;
logger.warn(`Stale PID file at ${filePath}, but the PID ${pid} does not belong to a running mempool instance`);
}
} else {
fs.writeFileSync(filePath, `${process.pid}`);
return true;
}
}
public releasePidLock(): void {
const filePath = path.join(config.DATABASE.PID_DIR || __dirname, `/mempool-${config.DATABASE.DATABASE}.pid`);
if (fs.existsSync(filePath)) {
const pid = fs.readFileSync(filePath).toString();
if (pid === `${process.pid}`) {
const pid = parseInt(fs.readFileSync(filePath, 'utf-8'));
// only release our own pid file
if (pid === process.pid) {
fs.unlinkSync(filePath);
}
}

View File

@@ -92,9 +92,15 @@ class Server {
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
// Register cleanup listeners for exit events
['exit', 'SIGHUP', 'SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2', 'uncaughtException', 'unhandledRejection'].forEach(event => {
['exit', 'SIGHUP', 'SIGINT', 'SIGTERM', 'SIGUSR1', 'SIGUSR2'].forEach(event => {
process.on(event, () => { this.onExit(event); });
});
process.on('uncaughtException', (error) => {
this.onUnhandledException('uncaughtException', error);
});
process.on('unhandledRejection', (reason, promise) => {
this.onUnhandledException('unhandledRejection', reason);
});
if (config.MEMPOOL.BACKEND === 'esplora') {
bitcoinApi.startHealthChecks();
@@ -200,7 +206,7 @@ class Server {
}
const newMempool = await bitcoinApi.$getRawMempool();
const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerRunning ? 10 : 1);
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool, pollRate);
}
@@ -314,14 +320,18 @@ class Server {
}
}
onExit(exitEvent): void {
onExit(exitEvent, code = 0): void {
logger.debug(`onExit for signal: ${exitEvent}`);
if (config.DATABASE.ENABLED) {
DB.releasePidLock();
}
process.exit(0);
process.exit(code);
}
onUnhandledException(type, error): void {
console.error(`${type}:`, error);
this.onExit(type, 1);
}
}
((): Server => new Server())();

View File

@@ -15,11 +15,18 @@ export interface CoreIndex {
best_block_height: number;
}
type TaskName = 'blocksPrices' | 'coinStatsIndex';
class Indexer {
runIndexer = true;
indexerRunning = false;
tasksRunning: string[] = [];
coreIndexes: CoreIndex[] = [];
private runIndexer = true;
private indexerRunning = false;
private tasksRunning: { [key in TaskName]?: boolean; } = {};
private tasksScheduled: { [key in TaskName]?: NodeJS.Timeout; } = {};
private coreIndexes: CoreIndex[] = [];
public indexerIsRunning(): boolean {
return this.indexerRunning;
}
/**
* Check which core index is available for indexing
@@ -69,33 +76,69 @@ class Indexer {
}
}
public async runSingleTask(task: 'blocksPrices' | 'coinStatsIndex'): Promise<void> {
if (!Common.indexingEnabled()) {
return;
}
if (task === 'blocksPrices' && !this.tasksRunning.includes(task) && !['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
this.tasksRunning.push(task);
const lastestPriceId = await PricesRepository.$getLatestPriceId();
if (priceUpdater.historyInserted === false || lastestPriceId === null) {
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
setTimeout(() => {
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
this.runSingleTask('blocksPrices');
}, 10000);
} else {
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
await mining.$indexBlockPrices();
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
/**
* schedules a single task to run in `timeout` ms
* only one task of each type may be scheduled
*
* @param {TaskName} task - the type of task
* @param {number} timeout - delay in ms
* @param {boolean} replace - `true` replaces any already scheduled task (works like a debounce), `false` ignores subsequent requests (works like a throttle)
*/
public scheduleSingleTask(task: TaskName, timeout: number = 10000, replace = false): void {
if (this.tasksScheduled[task]) {
if (!replace) { //throttle
return;
} else { // debounce
clearTimeout(this.tasksScheduled[task]);
}
}
this.tasksScheduled[task] = setTimeout(async () => {
try {
await this.runSingleTask(task);
} catch (e) {
logger.err(`Unexpected error in scheduled task ${task}: ` + (e instanceof Error ? e.message : e));
} finally {
clearTimeout(this.tasksScheduled[task]);
}
}, timeout);
}
if (task === 'coinStatsIndex' && !this.tasksRunning.includes(task)) {
this.tasksRunning.push(task);
logger.debug(`Indexing coinStatsIndex now`);
await mining.$indexCoinStatsIndex();
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
/**
* Runs a single task immediately
*
* (use `scheduleSingleTask` instead to queue a task to run after some timeout)
*/
public async runSingleTask(task: TaskName): Promise<void> {
if (!Common.indexingEnabled() || this.tasksRunning[task]) {
return;
}
this.tasksRunning[task] = true;
switch (task) {
case 'blocksPrices': {
if (!['testnet', 'signet'].includes(config.MEMPOOL.NETWORK)) {
let lastestPriceId;
try {
lastestPriceId = await PricesRepository.$getLatestPriceId();
} catch (e) {
logger.debug('failed to fetch latest price id from db: ' + (e instanceof Error ? e.message : e));
} if (priceUpdater.historyInserted === false || lastestPriceId === null) {
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
this.scheduleSingleTask(task, 10000);
} else {
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
await mining.$indexBlockPrices();
}
}
} break;
case 'coinStatsIndex': {
logger.debug(`Indexing coinStatsIndex now`);
await mining.$indexCoinStatsIndex();
} break;
}
this.tasksRunning[task] = false;
}
public async $run(): Promise<void> {

View File

@@ -157,4 +157,6 @@ class Logger {
}
}
export type LogLevel = 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
export default new Logger();

View File

@@ -14,7 +14,7 @@ class NodesSocketsRepository {
await DB.query(`
INSERT INTO nodes_sockets(public_key, socket, type)
VALUE (?, ?, ?)
`, [socket.publicKey, socket.addr, socket.network]);
`, [socket.publicKey, socket.addr, socket.network], 'silent');
} catch (e: any) {
if (e.errno !== 1062) { // ER_DUP_ENTRY - Not an issue, just ignore this
logger.err(`Cannot save node socket (${[socket.publicKey, socket.addr, socket.network]}) into db. Reason: ` + (e instanceof Error ? e.message : e));

View File

@@ -79,7 +79,7 @@ class ForensicsService {
}
let progress = 0;
const sliceLength = 1000;
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 10);
// process batches of 1000 channels
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) {
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength);

View File

@@ -290,7 +290,7 @@ class NetworkSyncService {
const allChannels = await channelsApi.$getChannelsByStatus([0, 1]);
const sliceLength = 5000;
const sliceLength = Math.ceil(config.ESPLORA.BATCH_QUERY_BASE_SIZE / 2);
// process batches of 5000 channels
for (let i = 0; i < Math.ceil(allChannels.length / sliceLength); i++) {
const channels = allChannels.slice(i * sliceLength, (i + 1) * sliceLength);

3
contributors/ncois.txt Normal file
View File

@@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 16, 2023.
Signed: ncois

View File

@@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 15, 2023.
Signed: shubhamkmr04

3
contributors/starius.txt Normal file
View File

@@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of Oct 13, 2023.
Signed starius

View File

@@ -53,6 +53,7 @@
"ESPLORA": {
"REST_API_URL": "__ESPLORA_REST_API_URL__",
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
"BATCH_QUERY_BASE_SIZE": __ESPLORA_BATCH_QUERY_BASE_SIZE__,
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__,
"REQUEST_TIMEOUT": __ESPLORA_REQUEST_TIMEOUT__,
"FALLBACK_TIMEOUT": __ESPLORA_FALLBACK_TIMEOUT__,
@@ -146,6 +147,7 @@
},
"REDIS": {
"ENABLED": __REDIS_ENABLED__,
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__",
"BATCH_QUERY_BASE_SIZE": __REDIS_BATCH_QUERY_BASE_SIZE__
}
}

View File

@@ -54,6 +54,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
# ESPLORA
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:="null"}
__ESPLORA_BATCH_QUERY_BASE_SIZE__=${ESPLORA_BATCH_QUERY_BASE_SIZE:=1000}
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
__ESPLORA_REQUEST_TIMEOUT__=${ESPLORA_REQUEST_TIMEOUT:=5000}
__ESPLORA_FALLBACK_TIMEOUT__=${ESPLORA_FALLBACK_TIMEOUT:=5000}
@@ -148,6 +149,7 @@ __MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
# REDIS
__REDIS_ENABLED__=${REDIS_ENABLED:=false}
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
@@ -201,6 +203,7 @@ sed -i "s!__ELECTRUM_TLS_ENABLED__!${__ELECTRUM_TLS_ENABLED__}!g" mempool-config
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json
sed -i "s!__ESPLORA_BATCH_QUERY_BASE_SIZE__!${__ESPLORA_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json
sed -i "s!__ESPLORA_REQUEST_TIMEOUT__!${__ESPLORA_REQUEST_TIMEOUT__}!g" mempool-config.json
sed -i "s!__ESPLORA_FALLBACK_TIMEOUT__!${__ESPLORA_FALLBACK_TIMEOUT__}!g" mempool-config.json
@@ -288,5 +291,6 @@ sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS_
# REDIS
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
sed -i "s!__REDIS_BATCH_QUERY_BASE_SIZE__!${__REDIS_BATCH_QUERY_BASE_SIZE__}!g" mempool-config.json
node /backend/package/index.js

View File

@@ -31,10 +31,10 @@
"bootstrap": "~4.6.2",
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"cypress": "^13.6.0",
"domino": "^2.1.6",
"echarts": "~5.4.3",
"lightweight-charts": "~3.8.0",
"mock-socket": "~9.3.1",
"ngx-echarts": "~16.2.0",
"ngx-infinite-scroll": "^16.0.0",
"qrcode": "1.5.1",
@@ -59,7 +59,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.5.0",
"cypress": "^13.6.0",
"cypress-fail-on-console-error": "~5.0.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",
@@ -7148,9 +7148,9 @@
"peer": true
},
"node_modules/cypress": {
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.5.0.tgz",
"integrity": "sha512-oh6U7h9w8wwHfzNDJQ6wVcAeXu31DlIYlNOBvfd6U4CcB8oe4akawQmH+QJVOMZlM42eBoCne015+svVqdwdRQ==",
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz",
"integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
@@ -22096,9 +22096,9 @@
"peer": true
},
"cypress": {
"version": "13.5.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.5.0.tgz",
"integrity": "sha512-oh6U7h9w8wwHfzNDJQ6wVcAeXu31DlIYlNOBvfd6U4CcB8oe4akawQmH+QJVOMZlM42eBoCne015+svVqdwdRQ==",
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.0.tgz",
"integrity": "sha512-quIsnFmtj4dBUEJYU4OH0H12bABJpSujvWexC24Ju1gTlKMJbeT6tTO0vh7WNfiBPPjoIXLN+OUqVtiKFs6SGw==",
"optional": true,
"requires": {
"@cypress/request": "^3.0.0",

View File

@@ -110,7 +110,7 @@
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",
"@types/cypress": "^1.1.3",
"cypress": "^13.5.0",
"cypress": "^13.6.0",
"cypress-fail-on-console-error": "~5.0.0",
"cypress-wait-until": "^2.0.1",
"mock-socket": "~9.3.1",

View File

@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AppPreloadingStrategy } from './app.preloading-strategy'
import { BlockViewComponent } from './components/block-view/block-view.component';
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
import { ClockComponent } from './components/clock/clock.component';
import { StatusViewComponent } from './components/status-view/status-view.component';
@@ -124,6 +125,10 @@ let routes: Routes = [
path: 'view/mempool-block/:index',
component: MempoolBlockViewComponent,
},
{
path: 'view/blocks',
component: EightBlocksComponent,
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },

View File

@@ -32,11 +32,22 @@
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
</video>
<ng-container *ngIf="false && officialMempoolSpace">
<h3 class="mt-5">Sponsor the project</h3>
<div class="d-flex justify-content-center" style="max-width: 90%; margin: 35px auto 75px auto; column-gap: 15px">
<a href="/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button">Community</a>
<a href="/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button">Enterprise</a>
<ng-container *ngIf="officialMempoolSpace">
<div id="become-sponsor-container">
<div class="become-sponsor community">
<p style="font-weight: 700; font-size: 18px;">If you're an individual...</p>
<a href="/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button">Become a Community Sponsor</a>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Exclusive swag</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Your avatar on the About page</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> And more coming soon :)</p>
</div>
<div class="become-sponsor enterprise">
<p style="font-weight: 700; font-size: 18px;">If you're a business...</p>
<a href="/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button">Become an Enterprise Sponsor</a>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Increased API limits</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Co-branded instance</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> 99% service-level agreement</p>
</div>
</div>
</ng-container>
@@ -193,7 +204,7 @@
<ng-container>
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</ng-container>
@@ -205,7 +216,7 @@
<div class="wrapper">
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
</a>
</ng-template>
</div>
@@ -299,9 +310,9 @@
<img class="image" src="/resources/profile/blixt.png" />
<span>Blixt</span>
</a>
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="Zeus">
<a href="https://github.com/ZeusLN/zeus" target="_blank" title="ZEUS">
<img class="image" src="/resources/profile/zeus.png" />
<span>Zeus</span>
<span>ZEUS</span>
</a>
<a href="https://github.com/vulpemventures/marina" target="_blank" title="Marina Wallet">
<img class="image" src="/resources/profile/marina.svg" />

View File

@@ -246,3 +246,48 @@
width: 64px;
height: 64px;
}
#become-sponsor-container {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
gap: 20px;
margin: 68px auto;
}
.become-sponsor {
background-color: #1d1f31;
border-radius: 16px;
padding: 12px 20px;
width: 400px;
padding: 40px 20px;
}
.become-sponsor a {
margin-top: 10px;
}
#become-sponsor-container .btn {
margin-bottom: 24px;
}
#become-sponsor-container .ng-fa-icon {
color: #2ecc71;
margin-right: 5px;
}
#become-sponsor-container .sponsor-feature {
text-align: left;
width: 250px;
margin: 12px auto;
}
@media (max-width: 992px) {
#become-sponsor-container {
flex-wrap: wrap;
}
}

View File

@@ -2,7 +2,6 @@
height: 100%;
min-width: 120px;
width: 120px;
max-height: 90vh;
margin-left: 4em;
margin-right: 1.5em;
padding-bottom: 63px;
@@ -18,6 +17,7 @@
bottom: 0;
left: 0;
right: 0;
min-height: 30px;
display: flex;
flex-direction: column;
justify-content: center;

View File

@@ -58,13 +58,15 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
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
});
if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) {
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),

View File

@@ -1,7 +1,7 @@
<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>.
Transaction has now been <a class="alert-link" routerLink="/services/accelerator/history">submitted</a> to mining pools for acceleration.
</div>
</div>
</div>
@@ -74,8 +74,8 @@
<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>
<span class="fee">{{ option.fee + estimate.mempoolBaseFee + estimate.vsizeFee | 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>
@@ -87,18 +87,10 @@
<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'">
<ng-container>
<tr class="group-first">
<td class="item">
Next block market rate
@@ -121,32 +113,6 @@
</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>
@@ -166,7 +132,7 @@
<span class="fiat"><app-fiat [value]="estimate.mempoolBaseFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
<tr class="info group-last">
<td class="info">
<i><small>Transaction vsize fee</small></i>
</td>
@@ -180,8 +146,8 @@
</tr>
<!-- NEXT BLOCK ESTIMATE -->
<ng-container *ngIf="showTable === 'estimated'">
<tr class="group-first">
<ng-container>
<tr class="group-first" style="border-top: 1px dashed grey; border-collapse: collapse;">
<td class="item">
<b style="background-color: #5E35B1" class="p-1 pl-0">Estimated acceleration cost</b>
</td>
@@ -195,7 +161,7 @@
<span class="fiat"><app-fiat [value]="estimate.cost + estimate.mempoolBaseFee + estimate.vsizeFee"></app-fiat></span>
</td>
</tr>
<tr class="info group-last">
<tr class="info group-last" style="border-bottom: 1px solid lightgrey">
<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>
@@ -203,7 +169,7 @@
</ng-container>
<!-- MAX COST -->
<ng-container *ngIf="showTable === 'maximum'">
<ng-container>
<tr class="group-first">
<td class="item">
<b style="background-color: #105fb0;" class="p-1 pl-0">Maximum acceleration cost</b>
@@ -228,7 +194,7 @@
</ng-container>
<!-- USER BALANCE -->
<ng-container *ngIf="estimate.userBalance < maxCost">
<ng-container *ngIf="isLoggedIn() && estimate.userBalance < maxCost">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item">
Available balance
@@ -244,6 +210,17 @@
</td>
</tr>
</ng-container>
<!-- LOGIN CTA -->
<ng-container *ngIf="!isLoggedIn()">
<tr class="group-first group-last" style="border-top: 1px dashed grey">
<td class="item"></td>
<td class="amt"></td>
<td class="units d-flex">
<a [routerLink]="['/login']" [queryParams]="{redirectTo: '/tx/' + tx.txid + '#accelerate'}" class="btn btn-purple flex-grow-1">Login</a>
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>

View File

@@ -28,7 +28,10 @@
.feerate.active {
background-color: #105fb0 !important;
opacity: 1;
border: 1px solid white !important;
border: 1px solid #007fff !important;
}
.feerate:focus {
box-shadow: none !important;
}
.estimateDisabled {
@@ -41,6 +44,24 @@
margin-top: 0.5em;
}
.tab {
&:first-child {
margin-right: 1px;
}
border: solid 1px black;
border-bottom: none;
background-color: #323655;
border-top-left-radius: 10px !important;
border-top-right-radius: 10px !important;
}
.tab.active {
background-color: #5d659d !important;
opacity: 1;
}
.tab:focus {
box-shadow: none !important;
}
.table-accelerator {
tr {
text-wrap: wrap;

View File

@@ -1,4 +1,5 @@
import { Component, OnInit, Input, OnDestroy, OnChanges, SimpleChanges, HostListener } from '@angular/core';
import { Router } from '@angular/router';
import { ApiService } from '../../services/api.service';
import { Subscription, catchError, of, tap } from 'rxjs';
import { StorageService } from '../../services/storage.service';
@@ -55,14 +56,14 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
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
private storageService: StorageService,
private router: Router,
) { }
ngOnDestroy(): void {
@@ -73,7 +74,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
ngOnChanges(changes: SimpleChanges): void {
if (changes.scrollEvent) {
this.scrollToPreview('acceleratePreviewAnchor', 'center');
this.scrollToPreview('acceleratePreviewAnchor', 'start');
}
}
@@ -126,7 +127,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.maxCost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
if (!this.error) {
this.scrollToPreview('acceleratePreviewAnchor', 'center');
this.scrollToPreview('acceleratePreviewAnchor', 'start');
}
}
}),
@@ -187,7 +188,11 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.estimateSubscription.unsubscribe();
},
error: (response) => {
this.error = response.error;
if (response.status === 403 && response.error === 'not_available') {
this.error = 'waitlisted';
} else {
this.error = response.error;
}
this.scrollToPreviewWithTimeout('mempoolError', 'center');
}
});

View File

@@ -1,4 +1,4 @@
<header>
<header class="sticky-header">
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">

View File

@@ -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;
}

View File

@@ -20,6 +20,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() blockLimit: number;
@Input() orientation = 'left';
@Input() flip = true;
@Input() animationDuration: number = 1000;
@Input() animationOffset: number | null = null;
@Input() disableSpinner = false;
@Input() mirrorTxid: string | void;
@Input() unavailable: boolean = false;
@@ -141,9 +143,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
}
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true): void {
replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
if (this.scene) {
this.scene.replace(transactions || [], direction, sort);
this.scene.replace(transactions || [], direction, sort, startTime);
this.start();
this.updateSearchHighlight();
}
@@ -226,7 +228,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
} else {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
highlighting: this.auditHighlighting });
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset });
this.start();
}
}

View File

@@ -9,6 +9,9 @@ export default class BlockScene {
txs: { [key: string]: TxView };
orientation: string;
flip: boolean;
animationDuration: number = 1000;
configAnimationOffset: number | null;
animationOffset: number;
highlightingEnabled: boolean;
width: number;
height: number;
@@ -23,11 +26,11 @@ export default class BlockScene {
animateUntil = 0;
dirty: boolean;
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number,
constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
) {
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting });
}
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
@@ -36,6 +39,7 @@ export default class BlockScene {
this.gridSize = this.width / this.gridWidth;
this.unitPadding = Math.max(1, Math.floor(this.gridSize / 5));
this.unitWidth = this.gridSize - (this.unitPadding * 2);
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
this.dirty = true;
if (this.initialised && this.scene) {
@@ -90,8 +94,8 @@ export default class BlockScene {
}
// Animate new block entering scene
enter(txs: TransactionStripped[], direction) {
this.replace(txs, direction);
enter(txs: TransactionStripped[], direction, startTime?: number) {
this.replace(txs, direction, false, startTime);
}
// Animate block leaving scene
@@ -108,8 +112,7 @@ export default class BlockScene {
}
// Reset layout and replace with new set of transactions
replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true): void {
const startTime = performance.now();
replace(txs: TransactionStripped[], direction: string = 'left', sort: boolean = true, startTime: number = performance.now()): void {
const nextIds = {};
const remove = [];
txs.forEach(tx => {
@@ -133,7 +136,7 @@ export default class BlockScene {
removed.forEach(tx => {
tx.destroy();
});
}, 1000);
}, (startTime - performance.now()) + this.animationDuration + 1000);
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
@@ -147,7 +150,7 @@ export default class BlockScene {
});
}
this.updateAll(startTime, 200, direction);
this.updateAll(startTime, 50, direction);
}
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
@@ -214,10 +217,13 @@ export default class BlockScene {
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
}
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number,
private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
): void {
this.animationDuration = animationDuration || 1000;
this.configAnimationOffset = animationOffset;
this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset;
this.orientation = orientation;
this.flip = flip;
this.vertexArray = vertexArray;
@@ -261,8 +267,8 @@ export default class BlockScene {
this.applyTxUpdate(tx, {
display: {
position: {
x: tx.screenPosition.x + (direction === 'right' ? -this.width : (direction === 'left' ? this.width : 0)) * 1.4,
y: tx.screenPosition.y + (direction === 'up' ? -this.height : (direction === 'down' ? this.height : 0)) * 1.4,
x: tx.screenPosition.x + (direction === 'right' ? -this.width - this.animationOffset : (direction === 'left' ? this.width + this.animationOffset : 0)),
y: tx.screenPosition.y + (direction === 'up' ? -this.height - this.animationOffset : (direction === 'down' ? this.height + this.animationOffset : 0)),
s: tx.screenPosition.s
},
color: txColor,
@@ -275,7 +281,7 @@ export default class BlockScene {
position: tx.screenPosition,
color: txColor
},
duration: animate ? 1000 : 1,
duration: animate ? this.animationDuration : 1,
start: startTime,
delay: animate ? delay : 0,
});
@@ -284,8 +290,8 @@ export default class BlockScene {
display: {
position: tx.screenPosition
},
duration: animate ? 1000 : 0,
minDuration: animate ? 500 : 0,
duration: animate ? this.animationDuration : 0,
minDuration: animate ? (this.animationDuration / 2) : 0,
start: startTime,
delay: animate ? delay : 0,
adjust: animate
@@ -322,11 +328,11 @@ export default class BlockScene {
this.applyTxUpdate(tx, {
display: {
position: {
x: tx.screenPosition.x + (direction === 'right' ? this.width : (direction === 'left' ? -this.width : 0)) * 1.4,
y: tx.screenPosition.y + (direction === 'up' ? this.height : (direction === 'down' ? -this.height : 0)) * 1.4,
x: tx.screenPosition.x + (direction === 'right' ? this.width + this.animationOffset : (direction === 'left' ? -this.width - this.animationOffset : 0)),
y: tx.screenPosition.y + (direction === 'up' ? this.height + this.animationOffset : (direction === 'down' ? -this.height - this.animationOffset : 0)),
}
},
duration: 1000,
duration: this.animationDuration,
start: startTime,
delay: 50
});

View File

@@ -0,0 +1,24 @@
<div class="blocks" [class.wrap]="wrapBlocks">
<ng-container *ngFor="let i of blockIndices">
<div class="block-wrapper" [style]="wrapperStyle">
<div class="block-container" [style]="containerStyle">
<app-block-overview-graph
#blockGraph
[isLoading]="false"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="'top'"
[flip]="false"
[animationDuration]="animationDuration"
[animationOffset]="animationOffset"
[disableSpinner]="true"
(txClickEvent)="onTxClick($event)"
></app-block-overview-graph>
<div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
<h1 class="height">{{ blockInfo[i].height }}</h1>
<h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }}</h2>
</div>
</div>
</div>
</ng-container>
</div>

View File

@@ -0,0 +1,69 @@
.blocks {
width: 100%;
height: 100%;
min-width: 100vw;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: flex-start;
align-content: flex-start;
&.wrap {
flex-wrap: wrap;
}
.block-wrapper {
flex-grow: 0;
flex-shrink: 0;
position: relative;
--block-width: 1080px;
.info {
position: absolute;
left: 8%;
top: 8%;
right: 8%;
bottom: 8%;
height: 84%;
width: 84%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: calc(var(--block-width) * 0.03);
text-shadow: 0 0 calc(var(--block-width) * 0.05) black;
h1 {
font-size: 6em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
h2 {
font-size: 1.8em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
.hash {
font-family: monospace;
word-wrap: break-word;
font-size: 1.4em;
line-height: 1;
margin-bottom: calc(var(--block-width) * 0.03);
}
.mined-by {
position: absolute;
bottom: 0;
margin: auto;
text-align: center;
}
}
}
.block-container {
overflow: hidden;
}
}

View File

@@ -0,0 +1,253 @@
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { catchError, startWith } from 'rxjs/operators';
import { Subject, Subscription, of } from 'rxjs';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
import { detectWebGL } from '../../shared/graphs.utils';
import { animate, style, transition, trigger } from '@angular/animations';
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
function bestFitResolution(min, max, n): number {
const target = (min + max) / 2;
let bestScore = Infinity;
let best = null;
for (let i = min; i <= max; i++) {
const remainder = (n % i);
if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
bestScore = remainder;
best = i;
}
}
return best;
}
interface BlockInfo extends BlockExtended {
timeString: string;
}
@Component({
selector: 'app-eight-blocks',
templateUrl: './eight-blocks.component.html',
styleUrls: ['./eight-blocks.component.scss'],
animations: [
trigger('infoChange', [
transition(':enter', [
style({ opacity: 0 }),
animate('1000ms', style({ opacity: 1 })),
]),
transition(':leave', [
animate('1000ms 500ms', style({ opacity: 0 }))
])
]),
],
})
export class EightBlocksComponent implements OnInit, OnDestroy {
network = '';
latestBlocks: BlockExtended[] = [];
isLoadingTransactions = true;
strippedTransactions: { [height: number]: TransactionStripped[] } = {};
webGlEnabled = true;
hoverTx: string | null = null;
blocksSubscription: Subscription;
cacheBlocksSubscription: Subscription;
networkChangedSubscription: Subscription;
queryParamsSubscription: Subscription;
graphChangeSubscription: Subscription;
numBlocks: number = 8;
blockIndices: number[] = [...Array(8).keys()];
autofit: boolean = false;
padding: number = 0;
wrapBlocks: boolean = false;
blockWidth: number = 1080;
animationDuration: number = 2000;
animationOffset: number = 0;
stagger: number = 0;
testing: boolean = true;
testHeight: number = 800000;
testShiftTimeout: number;
showInfo: boolean = true;
blockInfo: BlockInfo[] = [];
wrapperStyle = {
'--block-width': '1080px',
width: '1080px',
maxWidth: '1080px',
padding: '',
};
containerStyle = {};
resolution: number = 86;
@ViewChildren('blockGraph') blockGraphs: QueryList<BlockOverviewGraphComponent>;
constructor(
private route: ActivatedRoute,
private router: Router,
public stateService: StateService,
private websocketService: WebsocketService,
private apiService: ApiService,
private bytesPipe: BytesPipe,
) {
this.webGlEnabled = detectWebGL();
}
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.network = this.stateService.network;
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 8;
this.blockIndices = [...Array(this.numBlocks).keys()];
this.autofit = params.autofit !== 'false';
this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 10;
this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 540;
this.wrapBlocks = params.wrap !== 'false';
this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
this.animationOffset = this.padding * 2;
if (this.autofit) {
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
} else {
this.resolution = 86;
}
this.wrapperStyle = {
'--block-width': this.blockWidth + 'px',
width: this.blockWidth + 'px',
maxWidth: this.blockWidth + 'px',
padding: (this.padding || 0) +'px 0px',
};
if (params.test === 'true') {
if (this.blocksSubscription) {
this.blocksSubscription.unsubscribe();
}
this.blocksSubscription = (new Subject<BlockExtended[]>()).subscribe((blocks) => {
this.handleNewBlock(blocks.slice(0, this.numBlocks));
});
this.shiftTestBlocks();
} else if (!this.blocksSubscription) {
this.blocksSubscription = this.stateService.blocks$
.subscribe((blocks) => {
this.handleNewBlock(blocks.slice(0, this.numBlocks));
});
}
});
this.setupBlockGraphs();
this.networkChangedSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network);
}
ngAfterViewInit(): void {
this.graphChangeSubscription = this.blockGraphs.changes.pipe(startWith(null)).subscribe(() => {
this.setupBlockGraphs();
});
}
ngOnDestroy(): void {
this.stateService.markBlock$.next({});
if (this.blocksSubscription) {
this.blocksSubscription?.unsubscribe();
}
this.cacheBlocksSubscription?.unsubscribe();
this.networkChangedSubscription?.unsubscribe();
this.queryParamsSubscription?.unsubscribe();
}
shiftTestBlocks(): void {
const sub = this.apiService.getBlocks$(this.testHeight).subscribe(result => {
sub.unsubscribe();
this.handleNewBlock(result.slice(0, this.numBlocks));
this.testHeight++;
clearTimeout(this.testShiftTimeout);
this.testShiftTimeout = window.setTimeout(() => { this.shiftTestBlocks(); }, 10000);
});
}
async handleNewBlock(blocks: BlockExtended[]): Promise<void> {
const readyPromises: Promise<TransactionStripped[]>[] = [];
const previousBlocks = this.latestBlocks;
const newHeights = {};
this.latestBlocks = blocks;
for (const block of blocks) {
newHeights[block.height] = true;
if (!this.strippedTransactions[block.height]) {
readyPromises.push(new Promise((resolve) => {
const subscription = this.apiService.getStrippedBlockTransactions$(block.id).pipe(
catchError(() => {
return of([]);
}),
).subscribe((transactions) => {
this.strippedTransactions[block.height] = transactions;
subscription.unsubscribe();
resolve(transactions);
});
}));
}
}
await Promise.allSettled(readyPromises);
this.updateBlockGraphs(blocks);
// free up old transactions
previousBlocks.forEach(block => {
if (!newHeights[block.height]) {
delete this.strippedTransactions[block.height];
}
});
}
updateBlockGraphs(blocks): void {
const startTime = performance.now() + 1000 - (this.stagger < 0 ? this.stagger * 8 : 0);
if (this.blockGraphs) {
this.blockGraphs.forEach((graph, index) => {
graph.replace(this.strippedTransactions[blocks?.[index]?.height] || [], 'right', false, startTime + (this.stagger * index));
});
}
this.showInfo = false;
setTimeout(() => {
this.blockInfo = blocks.map(block => {
return {
...block,
timeString: (new Date(block.timestamp * 1000)).toLocaleTimeString(),
};
});
this.showInfo = true;
}, 1600); // Should match the animation time.
}
setupBlockGraphs(): void {
if (this.blockGraphs) {
this.blockGraphs.forEach((graph, index) => {
graph.destroy();
graph.setup(this.strippedTransactions[this.latestBlocks?.[index]?.height] || []);
});
}
}
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
if (!event.keyModifier) {
this.router.navigate([url]);
} else {
window.open(url, '_blank');
}
}
onTxHover(txid: string): void {
if (txid && txid.length) {
this.hoverTx = txid;
} else {
this.hoverTx = null;
}
}
}

View File

@@ -7,6 +7,8 @@ import { formatNumber } from '@angular/common';
import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs';
const OUTLIERS_MEDIAN_MULTIPLIER = 4;
@Component({
selector: 'app-incoming-transactions-graph',
templateUrl: './incoming-transactions-graph.component.html',
@@ -29,6 +31,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
@Input() left: number | string = '0';
@Input() template: ('widget' | 'advanced') = 'widget';
@Input() windowPreferenceOverride: string;
@Input() outlierCappingEnabled: boolean = false;
isLoading = true;
mempoolStatsChartOption: EChartsOption = {};
@@ -40,6 +43,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
MA: number[][] = [];
weightMode: boolean = false;
rateUnitSub: Subscription;
medianVbytesPerSecond: number | undefined;
constructor(
@Inject(LOCALE_ID) private locale: string,
@@ -65,16 +69,35 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference');
const windowSize = Math.max(10, Math.floor(this.data.series[0].length / 8));
this.MA = this.calculateMA(this.data.series[0], windowSize);
if (this.outlierCappingEnabled === true) {
this.computeMedianVbytesPerSecond(this.data.series[0]);
}
this.mountChart();
}
rendered() {
if (!this.data) {
return;
return;
}
this.isLoading = false;
}
/**
* Calculate the median value of the vbytes per second chart to hide outliers
*/
computeMedianVbytesPerSecond(data: number[][]): void {
const vBytes: number[] = [];
for (const value of data) {
vBytes.push(value[1]);
}
const sorted = vBytes.slice().sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);
this.medianVbytesPerSecond = sorted[middle];
if (sorted.length % 2 === 0) {
this.medianVbytesPerSecond = (sorted[middle - 1] + sorted[middle]) / 2;
}
}
/// calculate the moving average of the provided data based on windowSize
calculateMA(data: number[][], windowSize: number = 100): number[][] {
//update const variables that are not changed
@@ -232,6 +255,13 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
}
],
yAxis: {
max: (value) => {
if (!this.outlierCappingEnabled || value.max < this.medianVbytesPerSecond * OUTLIERS_MEDIAN_MULTIPLIER) {
return undefined;
} else {
return Math.round(this.medianVbytesPerSecond * OUTLIERS_MEDIAN_MULTIPLIER);
}
},
type: 'value',
axisLabel: {
fontSize: 11,

View File

@@ -1,5 +1,5 @@
<ng-container *ngIf="{ val: network$ | async } as network">
<header>
<header class="sticky-header">
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">

View File

@@ -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;
}

View File

@@ -5,7 +5,7 @@
<!-- 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">
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/images/' + 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)">
@@ -18,7 +18,7 @@
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
<ng-template [ngIf]="subdomain">
<div class="subdomain_container">
<img [src]="'/api/v1/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">
<img [src]="'/api/v1/services/enterprise/images/' + subdomain + '/logo'" class="subdomain_logo">
</div>
</ng-template>
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
@@ -71,13 +71,14 @@
<a class="nav-link" [routerLink]="['/about']" (click)="collapse()"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" i18n-title="master-page.about" title="About"></fa-icon></a>
</li>
</ul>
<app-search-form class="search-form-container" location="top" (searchTriggered)="collapse()"></app-search-form>
<app-search-form [hamburgerOpen]="user != null" class="search-form-container" location="top" (searchTriggered)="collapse()"></app-search-form>
</div>
</nav>
</header>
<div class="d-flex" style="overflow: clip">
<div class="d-flex">
<app-menu *ngIf="servicesEnabled" [navOpen]="menuOpen" (loggedOut)="onLoggedOut()" (menuToggled)="menuToggled($event)"></app-menu>
<div *ngIf="!servicesEnabled" class="sidenav"><!-- empty sidenav needed to push footer down the screen --></div>
<div class="flex-grow-1 d-flex flex-column">
<app-testnet-alert *ngIf="network.val === 'testnet' || network.val === 'signet'"></app-testnet-alert>

View File

@@ -238,4 +238,16 @@ nav {
main {
transition: 0.2s;
transition-property: max-width;
}
}
// empty sidenav
.sidenav {
z-index: 1;
background-color: transparent;
width: 225px;
height: calc(100vh - 65px);
position: sticky;
top: 65px;
margin-left: -225px;
padding-bottom: 20px;
}

View File

@@ -230,7 +230,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 100;
return positions;
},
extraCssText: `width: ${(this.template === 'advanced') ? '275px' : '200px'};
extraCssText: `width: ${(this.template === 'advanced') ? '300px' : '200px'};
background: transparent;
border: none;
box-shadow: none;`,
@@ -254,7 +254,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
const { totalValue, totalValueArray } = this.getTotalValues(params);
const itemFormatted = [];
let totalParcial = 0;
let sum = 0;
let progressPercentageText = '';
let countItem;
let items = this.inverted ? [...params].reverse() : params;
@@ -262,7 +262,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
countItem = items.pop();
}
items.map((item: any, index: number) => {
totalParcial += item.value[1];
sum += item.value[1];
const progressPercentage = (item.value[1] / totalValue) * 100;
const progressPercentageSum = (totalValueArray[index] / totalValue) * 100;
let activeItemClass = '';
@@ -279,7 +279,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
<span class="symbol">%</span>
</span>
<span class="total-parcial-vbytes">
${this.vbytesPipe.transform(totalParcial, 2, 'vB', 'MvB', false)}
${this.vbytesPipe.transform(sum, 2, 'vB', 'MvB', false)}
</span>
<div class="total-percentage-bar">
<span class="total-percentage-bar-background">
@@ -303,12 +303,12 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
</td>
<td class="total-progress-sum">
<span>
${this.vbytesPipe.transform(item.value[1], 2, 'vB', 'MvB', false)}
${(item.value[1] / 1_000_000).toFixed(2)} <span class="symbol">MvB</span>
</span>
</td>
<td class="total-progress-sum">
<span>
${this.vbytesPipe.transform(totalValueArray[index], 2, 'vB', 'MvB', false)}
${(totalValueArray[index] / 1_000_000).toFixed(2)} <span class="symbol">MvB</span>
</span>
</td>
<td class="total-progress-sum-bar">

View File

@@ -3,7 +3,7 @@
<nav class="scrollable menu-click">
<span *ngIf="userAuth" class="menu-click">
<strong class="menu-click">@ {{ userAuth.user.username }}</strong>
<strong class="menu-click text-nowrap ellipsis">@ {{ 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>

View File

@@ -5,15 +5,24 @@
height: calc(100vh - 65px);
position: sticky;
top: 65px;
transition: 0.25s;
transition: width 0.25s, left 0.25s, margin 0.25s;
margin-left: -250px;
box-shadow: 5px 0px 30px 0px #000;
padding-bottom: 20px;
@media (max-width: 613px) {
top: 105px;
}
}
.ellipsis {
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
.scrollable {
overflow-x: hidden;
overflow-y: scroll;
overflow-y: auto;
}
.sidenav.open {

View File

@@ -53,6 +53,26 @@
<br>
<h4>SIGNING UP FOR AN ACCOUNT ON MEMPOOL.SPACE</h4>
<p>If you sign up for an account on mempool.space, we may collect the following:</p>
<ol>
<li>If you provide your e-mail address, we may contact you regarding your account, billing purposess, or to update you about our services. We will not share this with any third-party.</li>
<li>If you connect your Twitter account, we may store your Twitter identity, e-mail address, and profile photo. We may publicly display your profile photo or link to your profile on our website, if you sponsor The Mempool Open Source Project, claim your Lightning node, or other such use cases.</li>
<li>If you make a credit card payment, we will process your payment using Square (Block, Inc.), and we will store details about the transaction in our database. Please see "Information we collect about customers" on Square's website at https://squareup.com/us/en/legal/general/privacy</li>
<li>If you make a Bitcoin or Liquid payment, we will process your payment using our self-hosted BTCPay Server instance and not share these details with any third-party.</li>
<li>If you accelerate transactions using Mempool Accelerator(tm), we will store the TXID of your transactions you accelerate with us. We share this information with our mining pool partners, as well as publicly display accelerated transaction details on our website and APIs.</li>
</ol>
<br>
<p>EOF</p>
</div>

View File

@@ -1,4 +1,4 @@
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
<form [class]="{hamburgerOpen: hamburgerOpen}" [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
<div class="d-flex">
<div class="search-box-container mr-2">
<input #searchInput (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">

View File

@@ -26,6 +26,13 @@ form {
@media (min-width: 992px) {
width: 100%;
}
&.hamburgerOpen {
@media (max-width: 613px) {
margin-left: 0px;
margin-right: 0px;
}
}
}
.btn-block {

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef, Input } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { EventType, NavigationStart, Router } from '@angular/router';
import { AssetsService } from '../../services/assets.service';
@@ -17,6 +17,8 @@ import { SearchResultsComponent } from './search-results/search-results.componen
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SearchFormComponent implements OnInit {
@Input() hamburgerOpen = false;
network = '';
assets: object = {};
isSearching = false;

View File

@@ -109,18 +109,26 @@
<div>
<div class="card mb-3">
<div class="card-header">
<div class="d-flex d-md-block align-items-baseline">
<span i18n="statistics.transaction-vbytes-per-second">Transaction vBytes per second (vB/s)</span>
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart('incoming')">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
</div>
<div class="vbytes-title">
<div>
<span i18n="statistics.transaction-vbytes-per-second">Transaction vBytes per second (vB/s)</span>
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart('incoming')">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
</div>
<div class="form-check">
<input style="margin-top: 9px" class="form-check-input" type="checkbox" [checked]="outlierCappingEnabled" id="hide-outliers" (change)="onOutlierToggleChange($event)">
<label class="form-check-label" for="hide-outliers">
<small i18n="statistics.cap-outliers">Cap outliers</small>
</label>
</div>
</div>
</div>
<div class="card-body">
<div class="incoming-transactions-graph">
<app-incoming-transactions-graph #incominggraph [height]="500" [left]="65" [template]="'advanced'"
[data]="mempoolTransactionsWeightPerSecondData"></app-incoming-transactions-graph>
[data]="mempoolTransactionsWeightPerSecondData" [outlierCappingEnabled]="outlierCappingEnabled"></app-incoming-transactions-graph>
</div>
</div>
</div>

View File

@@ -222,4 +222,13 @@
border-top-right-radius: 0;
}
}
}
.vbytes-title {
display: flex;
align-items: baseline;
justify-content: space-between;
@media (max-width: 767px) {
display: block;
}
}

View File

@@ -35,7 +35,7 @@ export class StatisticsComponent implements OnInit {
showCount = false;
maxFeeIndex: number;
dropDownOpen = false;
outlierCappingEnabled = false;
mempoolStats: OptimizedMempoolStats[] = [];
mempoolVsizeFeesData: any;
@@ -67,6 +67,7 @@ export class StatisticsComponent implements OnInit {
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';
this.outlierCappingEnabled = this.storageService.getValue('cap-outliers') === 'true';
this.radioGroupForm = this.formBuilder.group({
dateSpan: this.graphWindowPreference
@@ -156,8 +157,6 @@ export class StatisticsComponent implements OnInit {
}
this.maxFeeIndex = maxTier;
this.capExtremeVbytesValues();
this.mempoolTransactionsWeightPerSecondData = {
labels: labels,
series: [mempoolStats.map((stats) => [stats.added * 1000, stats.vbytes_per_second])],
@@ -211,36 +210,10 @@ export class StatisticsComponent implements OnInit {
}
});
}
/**
* All value higher that "median * capRatio" are capped
*/
capExtremeVbytesValues() {
if (this.stateService.network.length !== 0) {
return; // Only cap on Bitcoin mainnet
}
let capRatio = 10;
if (['1m', '3m', '6m', '1y', '2y', '3y', '4y'].includes(this.graphWindowPreference)) {
capRatio = 4;
}
// Find median value
const vBytes: number[] = [];
for (const stat of this.mempoolStats) {
vBytes.push(stat.vbytes_per_second);
}
const sorted = vBytes.slice().sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);
let median = sorted[middle];
if (sorted.length % 2 === 0) {
median = (sorted[middle - 1] + sorted[middle]) / 2;
}
// Cap
for (const stat of this.mempoolStats) {
stat.vbytes_per_second = Math.min(median * capRatio, stat.vbytes_per_second);
}
onOutlierToggleChange(e): void {
this.outlierCappingEnabled = e.target.checked;
this.storageService.setValue('cap-outliers', e.target.checked);
}
onSaveChart(name) {

View File

@@ -6,12 +6,15 @@
<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>
<!-- <div *ngIf="tx && acceleratorAvailable && accelerateCtaType === 'alert' && !tx.status.confirmed && !tx.acceleration" class="alert alert-dismissible alert-purple" role="alert">
<div>
<a class="btn btn-sm blink-bg" (click)="onAccelerateClicked()">Accelerate</a>
<span class="align-middle">this transaction using Mempool Accelerator &trade;</span>
</div>
<button type="button" class="close" aria-label="Close" (click)="dismissAccelAlert()">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div> -->
<ng-container *ngIf="!rbfTransaction || rbfTransaction?.size || tx">
<h1 i18n="shared.transaction">Transaction</h1>
@@ -80,13 +83,12 @@
<!-- Accelerator -->
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary">
<div class="title mt-3" id="acceleratePreviewAnchor">
<div class="title mt-3">
<h2>Accelerate</h2>
</div>
<div class="box">
<app-accelerate-preview [tx]="tx" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview>
</div>
</ng-container>
<ng-template #unconfirmedTemplate>
@@ -118,7 +120,7 @@
<ng-template [ngIf]="this.mempoolPosition.block >= 7" [ngIfElse]="belowBlockLimit">
<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>
<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>
<a *ngIf="!tx.acceleration && 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>
@@ -128,14 +130,14 @@
<ng-template #timeEstimateDefault>
<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>
<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>
<a *ngIf="!tx.acceleration && 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>
</ng-template>
</td>
</tr>
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" id="acceleratePreviewAnchor">
<td class="td-width" i18n="transaction.features|Transaction Features">Features</td>
<td>
<app-tx-features [tx]="tx"></app-tx-features>

View File

@@ -218,8 +218,33 @@
}
}
.link.accelerator {
cursor: pointer;
.alert-purple {
background-color: #5c3a88;
width: 100%;
}
// Blinking block
@keyframes shadowyBackground {
0% {
box-shadow: 0px 0px 20px rgba(#eba814, 1);
}
50% {
box-shadow: 0px 0px 20px rgba(#eba814, .3);
}
100% {
box-shadow: 0px 0px 20px rgba(#ffae00, 1);
}
}
.blink-bg {
color: #fff;
background: repeating-linear-gradient(#daad0a 0%, #daad0a 5%, #987805 100%) !important;
animation: shadowyBackground 1s infinite;
box-shadow: 0px 0px 20px rgba(#eba814, 1);
transition: 100ms all ease-in;
margin-right: 8px;
font-size: 16px;
border: 1px solid gold;
}
.eta {

View File

@@ -92,7 +92,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
rbfEnabled: boolean;
taprootEnabled: boolean;
hasEffectiveFeeRate: boolean;
accelerateCtaType: 'alert' | 'button' = 'alert';
accelerateCtaType: 'alert' | 'button' = 'button';
acceleratorAvailable: boolean = this.stateService.env.OFFICIAL_MEMPOOL_SPACE && this.stateService.env.ACCELERATOR && this.stateService.network === '';
showAccelerationSummary = false;
scrollIntoAccelPreview = false;
@@ -126,7 +126,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}
);
this.accelerateCtaType = (this.storageService.getValue('accel-cta-type') as 'alert' | 'button') ?? 'alert';
this.accelerateCtaType = (this.storageService.getValue('accel-cta-type') as 'alert' | 'button') ?? 'button';
this.setFlowEnabled();
this.flowPrefSubscription = this.stateService.hideFlow.subscribe((hide) => {
@@ -633,10 +633,14 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
// simulate normal anchor fragment behavior
applyFragment(): void {
const anchor = Array.from(this.fragmentParams.entries()).find(([frag, value]) => value === '');
if (anchor) {
const anchorElement = document.getElementById(anchor[0]);
if (anchorElement) {
anchorElement.scrollIntoView();
if (anchor?.length) {
if (anchor[0] === 'accelerate') {
setTimeout(this.onAccelerateClicked.bind(this), 100);
} else {
const anchorElement = document.getElementById(anchor[0]);
if (anchorElement) {
anchorElement.scrollIntoView();
}
}
}
}

View File

@@ -156,17 +156,45 @@ export class TransactionsListComponent implements OnInit, OnChanges {
}
if (this.address) {
const addressIn = tx.vout
.filter((v: Vout) => v.scriptpubkey_address === this.address)
.map((v: Vout) => v.value || 0)
.reduce((a: number, b: number) => a + b, 0);
const isP2PKUncompressed = this.address.length === 130;
const isP2PKCompressed = this.address.length === 66;
if (isP2PKCompressed) {
const addressIn = tx.vout
.filter((v: Vout) => v.scriptpubkey === '21' + this.address + 'ac')
.map((v: Vout) => v.value || 0)
.reduce((a: number, b: number) => a + b, 0);
const addressOut = tx.vin
.filter((v: Vin) => v.prevout && v.prevout.scriptpubkey_address === this.address)
.map((v: Vin) => v.prevout.value || 0)
.reduce((a: number, b: number) => a + b, 0);
const addressOut = tx.vin
.filter((v: Vin) => v.prevout && v.prevout.scriptpubkey === '21' + this.address + 'ac')
.map((v: Vin) => v.prevout.value || 0)
.reduce((a: number, b: number) => a + b, 0);
tx['addressValue'] = addressIn - addressOut;
tx['addressValue'] = addressIn - addressOut;
} else if (isP2PKUncompressed) {
const addressIn = tx.vout
.filter((v: Vout) => v.scriptpubkey === '41' + this.address + 'ac')
.map((v: Vout) => v.value || 0)
.reduce((a: number, b: number) => a + b, 0);
const addressOut = tx.vin
.filter((v: Vin) => v.prevout && v.prevout.scriptpubkey === '41' + this.address + 'ac')
.map((v: Vin) => v.prevout.value || 0)
.reduce((a: number, b: number) => a + b, 0);
tx['addressValue'] = addressIn - addressOut;
} else {
const addressIn = tx.vout
.filter((v: Vout) => v.scriptpubkey_address === this.address)
.map((v: Vout) => v.value || 0)
.reduce((a: number, b: number) => a + b, 0);
const addressOut = tx.vin
.filter((v: Vin) => v.prevout && v.prevout.scriptpubkey_address === this.address)
.map((v: Vin) => v.prevout.value || 0)
.reduce((a: number, b: number) => a + b, 0);
tx['addressValue'] = addressIn - addressOut;
}
}
this.priceService.getBlockPrice$(tx.status.block_time).pipe(

View File

@@ -237,7 +237,7 @@
<div class="card-text" *ngIf="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value; else loadingbig">
<div class="progress">
<div class="progress-bar {{ mempoolInfoData.value.mempoolSizeProgress }}" role="progressbar" [ngStyle]="{'width': (mempoolInfoData.value.memPoolInfo.usage / mempoolInfoData.value.memPoolInfo.maxmempool * 100) + '%' }">&nbsp;</div>
<div class="progress-text">&lrm;<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div>
<div class="progress-text">&lrm;<span [innerHTML]="mempoolInfoData.value.memPoolInfo.usage | bytes : 2 : 'B' : null : false : 3"></span> / <span [innerHTML]="mempoolInfoData.value.memPoolInfo.maxmempool | bytes"></span></div>
</div>
</div>
</div>

View File

@@ -109,12 +109,9 @@ export class MasterPageRoutingModule { }
],
declarations: [
MasterPageComponent,
],
exports: [
MasterPageComponent,
]
})
export class MasterPageModule { }

View File

@@ -311,7 +311,7 @@ export class ApiService {
}
getEnterpriseInfo$(name: string): Observable<any> {
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/enterprise/info/` + name);
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + `/api/v1/services/enterprise/info/` + name);
}
getChannelByTxIds$(txIds: string[]): Observable<any[]> {

View File

@@ -23,12 +23,12 @@
</div>
<a *ngIf="servicesEnabled" class="btn btn-purple sponsor d-none d-sm-flex justify-content-center" [routerLink]="['/login' | relativeUrl]">
<span *ngIf="loggedIn" i18n="shared.my-account">My Account</span>
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In / Sign Up</span>
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</span>
</a>
</div>
<a *ngIf="servicesEnabled" class="btn btn-purple sponsor d-flex d-sm-none justify-content-center ml-auto mr-auto mt-3 mb-2" [routerLink]="['/login' | relativeUrl]">
<span *ngIf="loggedIn" i18n="shared.my-account">My Account</span>
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In / Sign Up</span>
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In</span>
</a>
<p class="d-none d-sm-block">
<ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container>

View File

@@ -13,7 +13,7 @@ const MempoolErrors = {
'invalid_tx_dependencies': `This transaction dependencies are not valid.`,
'mempool_rejected_raw_tx': `Our mempool rejected this transaction`,
'no_mining_pool_available': `No mining pool available at the moment`,
'not_available': `You current subscription does not allow you to access this feature. Consider <strong><a style="color: #105fb0;" href="/sponsor" target="_blank">upgrading.</a><strong>`,
'not_available': `You current subscription does not allow you to access this feature.`,
'not_enough_balance': `Your account balance is too low. Please make a <a style="color:#105fb0" href="/services/accelerator/overview">deposit.</a>`,
'not_verified': `You must verify your account to use this feature.`,
'recommended_fees_not_available': `Recommended fees are not available right now.`,

View File

@@ -12,8 +12,10 @@
<ng-template #ltrTruncated>
<span class="first">{{text.slice(0,-lastChars)}}</span><span class="last-four">{{text.slice(-lastChars)}}</span>
<div class="hidden-content">{{ text }}</div>
</ng-template>
<ng-template #rtlTruncated>
<span class="first">{{text.slice(lastChars)}}</span><span class="last-four">{{text.slice(0,lastChars)}}</span>
<div class="hidden-content">{{ text }}</div>
</ng-template>

View File

@@ -3,6 +3,7 @@
display: flex;
flex-direction: row;
align-items: baseline;
position: relative;
.truncate-link {
display: flex;
@@ -27,4 +28,17 @@
&.inline {
display: inline-flex;
}
}
.hidden-content {
color: transparent;
position: absolute;
max-width: 300px;
overflow: hidden;
}
}
@media (max-width: 567px) {
.hidden-content {
max-width: 150px !important;
}
}

View File

@@ -1,6 +1,6 @@
/* tslint:disable */
import { Pipe, PipeTransform } from '@angular/core';
import { isNumberFinite, isPositive, isInteger, toDecimal } from './utils';
import { isNumberFinite, isPositive, isInteger, toDecimal, toSigFigs } from './utils';
export type ByteUnit = 'B' | 'kB' | 'MB' | 'GB' | 'TB';
@@ -17,7 +17,7 @@ export class BytesPipe implements PipeTransform {
'TB': {max: Number.MAX_SAFE_INTEGER, prev: 'GB'}
};
transform(input: any, decimal: number = 0, from: ByteUnit = 'B', to?: ByteUnit): any {
transform(input: any, decimal: number = 0, from: ByteUnit = 'B', to?: ByteUnit, plaintext = false, sigfigs?: number): any {
if (!(isNumberFinite(input) &&
isNumberFinite(decimal) &&
@@ -33,27 +33,35 @@ export class BytesPipe implements PipeTransform {
unit = BytesPipe.formats[unit].prev!;
}
let numberFormat = sigfigs == null ?
(number) => toDecimal(number, decimal).toString() :
(number) => toSigFigs(number, sigfigs);
if (to) {
const format = BytesPipe.formats[to];
const result = toDecimal(BytesPipe.calculateResult(format, bytes), decimal);
const result = numberFormat(BytesPipe.calculateResult(format, bytes));
return BytesPipe.formatResult(result, to);
return BytesPipe.formatResult(result, to, plaintext);
}
for (const key in BytesPipe.formats) {
const format = BytesPipe.formats[key];
if (bytes < format.max) {
const result = toDecimal(BytesPipe.calculateResult(format, bytes), decimal);
const result = numberFormat(BytesPipe.calculateResult(format, bytes));
return BytesPipe.formatResult(result, key);
return BytesPipe.formatResult(result, key, plaintext);
}
}
}
static formatResult(result: number, unit: string): string {
return `${result} <span class="symbol">${unit}</span>`;
static formatResult(result: string, unit: string, plaintext): string {
if (plaintext) {
return `${result} ${unit}`;
} else {
return `${result} <span class="symbol">${unit}</span>`;
}
}
static calculateResult(format: { max: number, prev?: ByteUnit }, bytes: number) {

View File

@@ -54,6 +54,10 @@ export function toDecimal(value: number, decimal: number): number {
return Math.round(value * Math.pow(10, decimal)) / Math.pow(10, decimal);
}
export function toSigFigs(value: number, sigFigs: number): string {
return value >= Math.pow(10, sigFigs - 1) ? Math.round(value).toString() : value.toPrecision(sigFigs);
}
export function upperFirst(value: string): string {
return value.slice(0, 1).toUpperCase() + value.slice(1);
}

View File

@@ -4,7 +4,7 @@ import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstra
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle } from '@fortawesome/free-solid-svg-icons';
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft, faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck, faCircleCheck, faUserCircle, faCheck } from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MenuComponent } from '../components/menu/menu.component';
import { PreviewTitleComponent } from '../components/master-page-preview/preview-title.component';
@@ -87,6 +87,7 @@ import { AccelerateFeeGraphComponent } from '../components/accelerate-preview/ac
import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component';
import { BlockViewComponent } from '../components/block-view/block-view.component';
import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component';
import { MempoolBlockViewComponent } from '../components/mempool-block-view/mempool-block-view.component';
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
import { ClockchainComponent } from '../components/clockchain/clockchain.component';
@@ -126,6 +127,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
ColoredPriceDirective,
BlockchainComponent,
BlockViewComponent,
EightBlocksComponent,
MempoolBlockViewComponent,
MempoolBlocksComponent,
BlockchainBlocksComponent,
@@ -179,6 +181,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
CalculatorComponent,
BitcoinsatoshisPipe,
BlockViewComponent,
EightBlocksComponent,
MempoolBlockViewComponent,
MempoolBlockOverviewComponent,
ClockchainComponent,
@@ -202,6 +205,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
FontAwesomeModule,
],
providers: [
BytesPipe,
VbytesPipe,
WuBytesPipe,
RelativeUrlPipe,
@@ -364,5 +368,6 @@ export class SharedModule {
library.addIcons(faUserCheck);
library.addIcons(faCircleCheck);
library.addIcons(faUserCircle);
library.addIcons(faCheck);
}
}

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" data-v-4fa90e7f=""><path d="M14.33 7.17C13.588 7.058 12.807 7 12 7c-4.97 0-9 2.239-9 5 0 1.44 1.096 2.738 2.85 3.65l2.362-2.362a4 4 0 015.076-5.076l1.043-1.043zM11.23 15.926a4 4 0 004.695-4.695l2.648-2.647C20.078 9.478 21 10.68 21 12c0 2.761-4.03 5-9 5-.598 0-1.183-.032-1.749-.094l.98-.98zM17.793 5.207a1 1 0 111.414 1.414L6.48 19.35a1 1 0 11-1.414-1.414L17.793 5.207z"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="lightgrey" data-v-4fa90e7f=""><path d="M14.33 7.17C13.588 7.058 12.807 7 12 7c-4.97 0-9 2.239-9 5 0 1.44 1.096 2.738 2.85 3.65l2.362-2.362a4 4 0 015.076-5.076l1.043-1.043zM11.23 15.926a4 4 0 004.695-4.695l2.648-2.647C20.078 9.478 21 10.68 21 12c0 2.761-4.03 5-9 5-.598 0-1.183-.032-1.749-.094l.98-.98zM17.793 5.207a1 1 0 111.414 1.414L6.48 19.35a1 1 0 11-1.414-1.414L17.793 5.207z"></path></svg>

Before

Width:  |  Height:  |  Size: 464 B

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -519,7 +519,7 @@ html:lang(ru) .card-title {
.fees-wrapper-tooltip-chart-advanced,
.tx-wrapper-tooltip-chart-advanced {
background: rgba(#1d1f31, 0.98);
width: 275px;
width: 300px;
thead {
th {

View File

@@ -394,7 +394,7 @@ FREEBSD_PKG=()
FREEBSD_PKG+=(zsh sudo git git-lfs screen curl wget calc neovim)
FREEBSD_PKG+=(openssh-portable py39-pip rust llvm10 jq base64 libzmq4)
FREEBSD_PKG+=(boost-libs autoconf automake gmake gcc libevent libtool pkgconf)
FREEBSD_PKG+=(nginx rsync py39-certbot-nginx mariadb105-server keybase)
FREEBSD_PKG+=(nginx rsync py39-certbot-nginx mariadb1011-server keybase)
FREEBSD_PKG+=(geoipupdate)
FREEBSD_UNFURL_PKG=()

View File

@@ -23,7 +23,7 @@
"PASSWORD": "__BITCOIN_RPC_PASS__"
},
"ESPLORA": {
"UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-mainnet",
"UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquid",
"FALLBACK": [
"http://node201.va1.mempool.space:3001",
"http://node202.va1.mempool.space:3001",

View File

@@ -23,7 +23,7 @@
"PASSWORD": "__BITCOIN_RPC_PASS__"
},
"ESPLORA": {
"UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-testnet",
"UNIX_SOCKET_PATH": "/elements/socket/esplora-elements-liquidtestnet",
"FALLBACK": [
"http://node201.va1.mempool.space:3004",
"http://node202.va1.mempool.space:3004",

View File

@@ -1,16 +1,19 @@
#!/usr/bin/env zsh
# kill "while true" loops
killall sh
# kill actual node backends
killall node
killall node 2>/dev/null
# kill "while true" loops
killall sh 2>/dev/null
# kill unfurler chrome instances
killall chrome
killall chrome 2>/dev/null
# kill xorg
killall xinit
killall xinit 2>/dev/null
# kill dbus
killall dbus-daemon 2>/dev/null
# kill nginx cache warmer scripts
for pid in `ps uaxww|grep warmer|grep zsh|awk '{print $2}'`;do

View File

@@ -1,96 +1,158 @@
location /api/v1/services {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 1d;
expires 1d;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
}
location /api/v1/contributors/images {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 1w;
expires 1w;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
###########
# routing #
###########
location /api/v1/assets {
try_files /dev/null @mempool-api-v1-services-cache-short;
}
location /api/v1/contributors {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 1d;
expires 1d;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
try_files /dev/null @mempool-api-v1-services-cache-medium;
}
location /api/v1/donations/images {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 1w;
expires 1w;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
location /api/v1/contributors/images {
try_files /dev/null @mempool-api-v1-services-cache-long;
}
location /api/v1/donations {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 1d;
expires 1d;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
try_files /dev/null @mempool-api-v1-services-cache-medium;
}
location /api/v1/translators/images {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 1w;
expires 1w;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
location /api/v1/donations/images {
try_files /dev/null @mempool-api-v1-services-cache-long;
}
location /api/v1/translators {
try_files /dev/null @mempool-api-v1-services-cache-medium;
}
location /api/v1/translators/images {
try_files /dev/null @mempool-api-v1-services-cache-long;
}
location /api/v1/services/internal/ {
return 403;
}
location /api/v1/services/ {
try_files /dev/null @mempool-api-v1-services-cache-disabled;
}
###########
# caching #
###########
location @mempool-api-v1-services-cache-disabled {
proxy_pass $mempoolSpaceServices;
# remove these just in case double proxied
proxy_hide_header Onion-Location;
proxy_hide_header Strict-Transport-Security;
proxy_hide_header Content-Security-Policy;
proxy_hide_header X-Frame-Options;
# remove cache headers from services backend
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
proxy_hide_header Pragma;
# pass these headers to services backend
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# add our own cache headers
add_header 'Pragma' 'no-cache';
add_header 'Cache-Control' 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
proxy_redirect off;
proxy_buffering off;
expires -1;
}
location @mempool-api-v1-services-cache-short {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 1d;
expires 1d;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
proxy_redirect off;
# remove these just in case double proxied
proxy_hide_header Onion-Location;
proxy_hide_header Strict-Transport-Security;
proxy_hide_header Content-Security-Policy;
proxy_hide_header X-Frame-Options;
# pass these headers to services backend
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# remove cache headers from services backend
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
proxy_hide_header Pragma;
# add our own cache headers
add_header 'Pragma' 'public';
add_header 'Cache-Control' 'public';
expires 2s;
# server-side cache validity
proxy_cache_valid 200 2s;
}
location /api/v1/assets {
location @mempool-api-v1-services-cache-medium {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache_valid 200 10m;
expires 10m;
proxy_hide_header onion-location;
proxy_hide_header strict-transport-security;
proxy_hide_header content-security-policy;
proxy_hide_header x-frame-options;
proxy_redirect off;
# remove these just in case double proxied
proxy_hide_header Onion-Location;
proxy_hide_header Strict-Transport-Security;
proxy_hide_header Content-Security-Policy;
proxy_hide_header X-Frame-Options;
# pass these headers to services backend
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# remove cache headers from services backend
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
proxy_hide_header Pragma;
# add our own cache headers
add_header 'Pragma' 'public';
add_header 'Cache-Control' 'public';
expires 7d;
# server-side cache validity
proxy_cache_valid 200 7d;
}
location @mempool-api-v1-services-cache-long {
proxy_pass $mempoolSpaceServices;
proxy_cache services;
proxy_redirect off;
# remove these just in case double proxied
proxy_hide_header Onion-Location;
proxy_hide_header Strict-Transport-Security;
proxy_hide_header Content-Security-Policy;
proxy_hide_header X-Frame-Options;
# pass these headers to services backend
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# remove cache headers from services backend
proxy_hide_header Cache-Control;
proxy_hide_header Expires;
proxy_hide_header Pragma;
# add our own cache headers
add_header 'Pragma' 'public';
add_header 'Cache-Control' 'public';
expires 30d;
# server-side cache validity
proxy_cache_valid 200 30d;
}

View File

@@ -4,10 +4,10 @@
# Block the internal APIs of esplora
location /api/internal/ {
return 404;
return 403;
}
location /api/v1/internal/ {
return 404;
return 403;
}
# websocket has special HTTP headers