Compare commits

...

117 Commits

Author SHA1 Message Date
natsoni
865015a193 Adaptive decimal number length in amount shortener 2024-12-07 21:31:49 +01:00
wiz
a0596cd366 Merge pull request #5654 from mempool/natsoni/address-graph-fix
Fix address balance graph
2024-12-06 17:17:08 +09:00
wiz
0f14aa7ad3 Merge pull request #5635 from mempool/natsoni/refactor-clipboard
Refactor clipboard component
2024-12-06 17:16:26 +09:00
wiz
44a0f92cc1 Merge pull request #5627 from mempool/mononaut/custom-dash-assets
update custom dashboard assets
2024-12-06 17:15:47 +09:00
wiz
97a9ea47fc Merge pull request #5641 from mempool/mononaut/fix-mempool-network-change
fix stuck mempool block on network change
2024-12-06 17:14:35 +09:00
wiz
d573147ad4 Remove unnecessary par=16 from bitcoin.conf 2024-12-06 15:14:26 +09:00
wiz
679745fb6c Merge pull request #5668 from mempool/natsoni/fix-accel-cancellation
Fix tx frontend issues after acceleration cancellation
2024-12-06 14:30:04 +09:00
wiz
c089920e4b Merge pull request #5666 from mempool/mononaut/fix-undefined-mempooltxs
fix undefined mempool tx errors
2024-12-06 14:29:47 +09:00
softsimon
62c96272d8 Merge pull request #5669 from mempool/natsoni/fix-liquid
Fix liquid database index
2024-12-05 21:55:35 +07:00
natsoni
ebd4170da1 Fix liquid database index 2024-12-04 16:14:51 +01:00
natsoni
0310452dfb Fix tx frontend issues after acceleration cancellation 2024-12-04 15:45:18 +01:00
wiz
969687ef39 Merge pull request #5662 from mempool/nymkappa/remove-useless-unique-uuid-accel
[accelerator] remove useless accelerationUUID
2024-12-04 17:15:09 +09:00
wiz
0831256cce Merge pull request #5663 from mempool/nymkappa/acceleration-cancel
[doc] add accelerator cancel doc
2024-12-04 16:41:54 +09:00
wiz
af40cac284 Merge pull request #5643 from mempool/mononaut/liquid-db-indexes
liquid db indexes
2024-12-04 16:37:40 +09:00
Mononaut
e4868b70c1 more processBlockTemplates null checks 2024-12-02 22:51:24 +00:00
Mononaut
5f45ce80f1 filter accelerations before calculating pool positions 2024-12-02 22:51:23 +00:00
softsimon
14e49126c3 Merge pull request #5642 from mempool/mononaut/db-indexes
Add missing db indexes
2024-11-28 15:03:43 +07:00
nymkappa
a4d73130b7 [doc] add accelerator cancel doc 2024-11-26 17:30:11 +01:00
nymkappa
cd702955fc [accelerator] remove useless accelerationUUID 2024-11-26 17:14:47 +01:00
wiz
4c66bf61f0 Merge pull request #5649 from mempool/mononaut/fix-acc-ws-timeout
fix acceleration websocket timeout loop
2024-11-22 07:21:10 -10:00
Mononaut
ffa582558b more verbose accelerator websocket error logs 2024-11-21 21:54:19 +00:00
Mononaut
9a81db8e6c fix acceleration websocket ping error 2024-11-19 23:00:28 +00:00
natsoni
cb3326d691 Wrap large amounts in power of ten in address graph 2024-11-19 19:32:28 +01:00
natsoni
535e5313ef Polish address balance graph tooltip 2024-11-19 18:11:02 +01:00
natsoni
8bd6d40ed2 Don't use SI units in address balance graph axis 2024-11-19 18:00:00 +01:00
natsoni
7516db0c71 Fix USD y axis overflow in address graph 2024-11-19 17:45:09 +01:00
Mononaut
abe9aa1fdc fix acceleration websocket timeout loop 2024-11-18 20:40:08 +00:00
Mononaut
60b3f9ace6 update custom dashboard assets 2024-11-17 16:05:48 +00:00
wiz
073fe8e8cd Merge pull request #5645 from mempool/nymkappa/accelerator-back-button-payment 2024-11-16 19:28:10 +09:00
nymkappa
bfe7b996a4 [accelerator] fix "Go back" button breaking payment flow 2024-11-16 11:23:09 +01:00
Mononaut
bfd771056d split new liquid db indexes into separate migrations 2024-11-16 00:01:13 +00:00
Mononaut
e05f5ee751 Add missing liquid db indexes 2024-11-16 00:00:18 +00:00
Mononaut
d9f3611da3 split new db indexes into separate migrations 2024-11-15 23:54:26 +00:00
Mononaut
7c7419ab1c Add missing db indexes 2024-11-15 22:30:46 +00:00
Mononaut
96afbca029 fix stuck mempool block on network change 2024-11-15 01:34:06 +00:00
softsimon
8c80358e71 Merge pull request #5634 from mempool/simon/fix-broken-cpfp-button
fix broken cpfp button
2024-11-15 08:29:02 +07:00
wiz
a5fbc94182 Merge pull request #5638 from mempool/mononaut/fix-acc-list-sub 2024-11-15 00:51:18 +09:00
Mononaut
fd7f340854 Fix acceleration list observable subscription logic 2024-11-14 02:40:41 +00:00
wiz
7f784944af ops: Fix install script nginx config parse error 2024-11-14 07:37:01 +09:00
natsoni
cab01f7f26 Refactor clipboard to use native clipboard API 2024-11-12 17:09:58 +01:00
softsimon
3b4eda432f fix cpfp list title position 2024-11-12 03:22:16 +07:00
softsimon
8b699da721 fix broken cpfp button 2024-11-12 03:18:22 +07:00
wiz
5b2f613856 Merge pull request #5633 from mempool/mononaut/fix-acc-ping-pong
Fix acceleration websocket ping/pong
2024-11-12 02:25:15 +09:00
Mononaut
f1e2c893cc Fix acceleration websocket ping/pong 2024-11-11 17:23:30 +00:00
wiz
7d3d59c348 Merge pull request #5632 from mempool/mononaut/acc-ws-ping
regularly ping acceleration websocket server
2024-11-12 02:08:48 +09:00
wiz
8719b424e5 Increase websocket from 5s to 10s 2024-11-12 02:03:56 +09:00
Mononaut
ef498b55ed reset acceleration websocket if unresponsive 2024-11-11 16:59:08 +00:00
Mononaut
9718610104 regularly ping acceleration websocket server 2024-11-11 16:26:30 +00:00
wiz
937e82bb89 Merge pull request #5631 from mempool/mononaut/acc-ws-debug
Better debug logs for accelerator websocket
2024-11-12 00:32:54 +09:00
Mononaut
91bf35bb65 Better debug logs for accelerator websocket 2024-11-11 14:40:25 +00:00
wiz
f0f6ee1919 Merge pull request #5630 from mempool/mononaut/fix-acc-websocket
fix acceleration websocket protocol
2024-11-11 20:53:27 +09:00
Mononaut
7b837b96da fix acceleration websocket protocol 2024-11-11 11:46:17 +00:00
softsimon
c417470be2 Merge pull request #5621 from mempool/natsoni/tx-first-seen-fix
Show tx first seen time with audit disabled
2024-11-09 00:28:47 +07:00
softsimon
dfd7877f82 Merge pull request #5584 from mempool/simon/cpfp-polish
Polish CPFP button
2024-11-09 00:25:05 +07:00
softsimon
136e80e5cf Merge pull request #5625 from mempool/natsoni/fix-url-nav
Fix navigation to use relative paths
2024-11-09 00:08:52 +07:00
natsoni
b3aed2f58b Fix navigation to use relative paths 2024-11-02 10:41:37 +01:00
wiz
e75f913af3 ops: Cache testnet electrs endpoints for minimum of 1s 2024-11-01 21:20:54 +09:00
wiz
0a9703f164 ops: Cache all electrs endpoints for minimum of 1s 2024-11-01 21:15:07 +09:00
natsoni
db321c3fa5 Get tx first seen from block summary if not available in audit 2024-10-30 15:43:28 +01:00
natsoni
b6aeb5661f Add block/:hash/tx/:txid/summary endpoint 2024-10-30 15:31:01 +01:00
natsoni
f08fa034cc Add missing frontend audit flag for testnet4 2024-10-30 15:19:46 +01:00
wiz
c0ef01d4da Merge pull request #5617 from vostrnad/fix-missing-fakescripthash
Add missing fake_scripthash to the data filter
2024-10-30 19:11:08 +09:00
wiz
c6711d8191 Merge pull request #5619 from mempool/nymkappa/fix-zindex-loading-indicator
[ui] fix loading indicator zindex
2024-10-30 19:10:51 +09:00
wiz
216bd5ad23 Merge pull request #5618 from mempool/nymkappa/fix-pool-blocks-page-length
[mining] return 100 blocks per page instead of 10 for pool block list
2024-10-30 19:10:37 +09:00
wiz
a7d59d6b2e Merge pull request #5620 from mempool/nymkappa/pool-hashrate 2024-10-30 18:51:36 +09:00
nymkappa
a257bcc12a [mining] show pools estimated hashrate on 3d and 1w timeframes 2024-10-30 10:46:44 +01:00
wiz
66c0ea7ca3 Merge pull request #5500 from mempool/mononaut/esplora-monitoring-reorg
Fix reorg to lower height on /monitoring status page
2024-10-30 18:28:35 +09:00
wiz
7692b2af66 Merge pull request #5511 from mempool/mononaut/summary-indexing-limit
respect INDEXING_BLOCKS_AMOUNT during summary indexing
2024-10-30 18:27:26 +09:00
wiz
397f53f42d Merge branch 'master' into mononaut/esplora-monitoring-reorg 2024-10-30 18:18:16 +09:00
wiz
2ea76d9c38 Merge pull request #5394 from mempool/mononaut/use-acceleration-websocket
[accelerator] get acceleration updates over websocket
2024-10-30 18:17:26 +09:00
nymkappa
59ac27b104 [ui] fix loading indicator zindex 2024-10-30 09:59:41 +01:00
nymkappa
d27bb7e156 [mining] return 100 blocks per page instead of 10 for pool block list 2024-10-30 09:54:13 +01:00
Vojtěch Strnad
4eadfc0a3b Add missing fake_scripthash to the data filter 2024-10-29 07:25:36 +01:00
wiz
76cfa3ca47 Merge pull request #5615 from mempool/mononaut/adjust-app-imports 2024-10-27 13:55:58 +09:00
Mononaut
3a4a4d9ffd don't allow overriding critical @app imports 2024-10-27 02:39:55 +00:00
wiz
8b01a83948 Increase time of demo mode from 3s to 15s 2024-10-25 16:49:24 +09:00
wiz
2ceb9001a1 Merge pull request #5613 from mempool/mononaut/fix-purple-pie-chart
fix purple pie chart with single pool
2024-10-25 15:13:06 +09:00
wiz
d44b7926d2 Merge pull request #5611 from mempool/nymkappa/demo-mode-but-it-does-not-crash-the-tv-anymore-well-maybe-we-ll-see-about-this
[demo] better? demo mode
2024-10-25 14:48:59 +09:00
wiz
57299e086e Remove /graphs from demo routes 2024-10-25 14:45:45 +09:00
Mononaut
c1d17dac43 fix purple pie chart with single pool 2024-10-25 05:39:52 +00:00
wiz
cb49f9d929 Merge pull request #5612 from mempool/nymkappa/internal-core-rpc-proxy
[core routes] /api/internal -> /api/v1/internal
2024-10-25 14:09:03 +09:00
nymkappa
185be3d598 [core routes] use config.MEMPOOL.API_URL_PREFIX 2024-10-25 14:02:09 +09:00
softsimon
aa9888a2fe Related Transactions 2024-10-25 11:58:01 +07:00
nymkappa
c950e3d0ae [core routes] /api/internal -> /api/v1/internal 2024-10-25 13:54:40 +09:00
softsimon
3909148d6e Polish CPFP button 2024-10-25 11:52:01 +07:00
nymkappa
99cc47cf00 [demo] better? demo mode 2024-10-24 16:52:47 +09:00
wiz
908b8b4352 Merge pull request #5606 from mempool/nymkappa/configurable-prod-domains
make prod domains configurable
2024-10-23 22:58:56 +09:00
nymkappa
1a7f475220 make prod domains configurable 2024-10-23 22:51:04 +09:00
wiz
cb63d17a2f ops: Don't always set frameoptions in nginx 2024-10-23 22:12:40 +09:00
wiz
c8ce4631e2 Merge pull request #5605 from mempool/mononaut/tx-extras-module
Refactor transaction page component
2024-10-23 22:00:39 +09:00
Mononaut
96c2b0a2f7 fix cpfp button 2024-10-23 12:28:40 +00:00
Mononaut
23475c7a1b Refactor transaction page component 2024-10-23 08:56:27 +00:00
wiz
9ab3d3195e Merge pull request #5604 from mempool/wiz/use-typescript-buildtime-path-aliases
Use typescript path aliases for build time import path resolution
2024-10-23 12:01:36 +09:00
wiz
a22d07ae60 Rename some more relative paths to use @app path alias 2024-10-23 11:46:33 +09:00
wiz
221658f6bf Change @app/interfaces to new @interfaces path alias 2024-10-23 11:09:38 +09:00
wiz
133df2e4be Use typescript path aliases for build time import path resolution 2024-10-22 23:30:19 +09:00
wiz
5fba9595af Merge pull request #5601 from mempool/nymkappa/demo-mode
implement very simple demo mode
2024-10-22 15:16:45 +09:00
wiz
90ca77a46a Tweak demo mode to use all dashboards 2024-10-22 15:08:57 +09:00
nymkappa
f0c76c1349 implement very simple demo mode 2024-10-19 16:07:09 +09:00
wiz
6e5cfa9bf2 Merge pull request #4831 from mempool/mononaut/wallet
Add multi-address wallet page
2024-10-18 13:05:17 +09:00
wiz
9ffcf2eca5 ops: Enable wallets in prod mempool backend config 2024-10-18 12:32:42 +09:00
wiz
8514fb9bdc Merge branch 'master' into mononaut/wallet 2024-10-18 12:08:05 +09:00
wiz
c1be7460c0 Merge pull request #5375 from mempool/mononaut/wallet-dashboard-widgets
custom wallet dashboard widgets
2024-10-18 12:06:02 +09:00
Mononaut
602aa4f948 fix wallet merge conflicts 2024-10-18 03:02:30 +00:00
Mononaut
2d2c55ce0e Add link to wallet page from custom dashboard widget 2024-10-18 02:41:44 +00:00
Mononaut
f0e207dff2 fix wallet balance graph bug 2024-10-18 02:41:44 +00:00
Mononaut
756e4356a5 named wallet sync track txo stats 2024-10-18 02:41:44 +00:00
Mononaut
9c303e8c23 address wallet page by name 2024-10-18 02:41:43 +00:00
Mononaut
a7ba4a0be8 Add multi-address wallet page 2024-10-18 02:41:18 +00:00
Mononaut
54c2d7efe5 move custom wallet sync to services backend 2024-10-18 02:35:19 +00:00
Mononaut
3f8eb3a2cd check in new resources 2024-10-18 02:35:19 +00:00
Mononaut
e095192968 custom dashboard wallet widgets 2024-10-18 02:35:17 +00:00
Mononaut
862c9591a1 wallet tracking backend support 2024-10-18 02:34:25 +00:00
softsimon
e6980a832b Merge branch 'master' into mononaut/use-acceleration-websocket 2024-10-07 15:58:13 +09:00
Mononaut
be49f70b09 [accelerator] get acceleration updates over websocket 2024-09-23 17:17:28 +00:00
Mononaut
7865574bd4 block summary indexing respect INDEXING_BLOCKS_AMOUNT 2024-09-11 21:48:26 +00:00
Mononaut
e513f05c09 Fix reorg to lower height on /monitoring status page 2024-09-05 20:55:48 +00:00
320 changed files with 4146 additions and 1959 deletions

View File

@@ -30,6 +30,7 @@ export interface AbstractBitcoinApi {
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
$getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>;
$getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]>;
startHealthChecks(): void;
getHealthStatus(): HealthCheckHost[];

View File

@@ -255,6 +255,10 @@ class BitcoinApi implements AbstractBitcoinApi {
return this.$getRawTransaction(txids[0]);
}
async $getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]> {
throw new Error('Method getAddressTransactionSummary not supported by the Bitcoin RPC API.');
}
$getEstimatedHashrate(blockHeight: number): Promise<number> {
// 120 is the default block span in Core
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);

View File

@@ -1,6 +1,7 @@
import { Application, NextFunction, Request, Response } from 'express';
import logger from '../../logger';
import bitcoinClient from './bitcoin-client';
import config from '../../config';
/**
* Define a set of routes used by the accelerator server
@@ -11,15 +12,15 @@ class BitcoinBackendRoutes {
public initRoutes(app: Application) {
app
.get('/api/internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
.post('/api/internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
.get('/api/internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
.post('/api/internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
.post('/api/internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
.get('/api/internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
.get('/api/internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
.get('/api/internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash)
.get('/api/internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount)
;
}

View File

@@ -42,6 +42,7 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/summary', this.getStrippedBlockTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
@@ -321,6 +322,20 @@ class BitcoinRoutes {
}
}
private async getStrippedBlockTransaction(req: Request, res: Response) {
try {
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
if (!transaction) {
handleError(req, res, 404, `transaction not found in summary`);
return;
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transaction);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async getBlock(req: Request, res: Response) {
try {
const block = await blocks.$getBlock(req.params.hash);

View File

@@ -179,4 +179,11 @@ export namespace IEsploraApi {
burn_count: number;
}
export interface AddressTxSummary {
txid: string;
value: number;
height: number;
time: number;
tx_position?: number;
}
}

View File

@@ -305,7 +305,7 @@ class ElectrsApi implements AbstractBitcoinApi {
}
$getAddress(address: string): Promise<IEsploraApi.Address> {
throw new Error('Method getAddress not implemented.');
return this.failoverRouter.$get<IEsploraApi.Address>('/address/' + address);
}
$getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
@@ -361,6 +361,10 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txid);
}
async $getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]> {
return this.failoverRouter.$get<IEsploraApi.AddressTxSummary[]>('/address/' + address + '/txs/summary');
}
public startHealthChecks(): void {
this.failoverRouter.startHealthChecks();
}

View File

@@ -412,8 +412,16 @@ class Blocks {
}
try {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
const currentBlockHeight = blockchainInfo.blocks;
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight);
if (indexingBlockAmount <= -1) {
indexingBlockAmount = currentBlockHeight + 1;
}
const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
// Get all indexed block hash
const indexedBlocks = await blocksRepository.$getIndexedBlocks();
const indexedBlocks = (await blocksRepository.$getIndexedBlocks()).filter(block => block.height >= lastBlockToIndex);
const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
@@ -1216,6 +1224,11 @@ class Blocks {
return summary.transactions;
}
public async $getSingleTxFromSummary(hash: string, txid: string): Promise<TransactionClassified | null> {
const txs = await this.$getStrippedBlockTransactions(hash);
return txs.find(tx => tx.txid === txid) || null;
}
/**
* Get 15 blocks
*

View File

@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2';
class DatabaseMigration {
private static currentVersion = 83;
private static currentVersion = 93;
private queryTimeout = 3600_000;
private statisticsAddedIndexed = false;
private uniqueLogs: string[] = [];
@@ -710,6 +710,97 @@ class DatabaseMigration {
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
await this.updateToSchemaVersion(83);
}
// add new pools indexes
if (databaseSchemaVersion < 84 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`pools\`
ADD INDEX \`slug\` (\`slug\`),
ADD INDEX \`unique_id\` (\`unique_id\`)
`);
await this.updateToSchemaVersion(84);
}
// lightning channels indexes
if (databaseSchemaVersion < 85 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`channels\`
ADD INDEX \`created\` (\`created\`),
ADD INDEX \`capacity\` (\`capacity\`),
ADD INDEX \`closing_reason\` (\`closing_reason\`),
ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
`);
await this.updateToSchemaVersion(85);
}
// lightning nodes indexes
if (databaseSchemaVersion < 86 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`nodes\`
ADD INDEX \`status\` (\`status\`),
ADD INDEX \`channels\` (\`channels\`),
ADD INDEX \`country_id\` (\`country_id\`),
ADD INDEX \`as_number\` (\`as_number\`),
ADD INDEX \`first_seen\` (\`first_seen\`)
`);
await this.updateToSchemaVersion(86);
}
// lightning node sockets indexes
if (databaseSchemaVersion < 87 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(87);
}
// lightning stats indexes
if (databaseSchemaVersion < 88 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
await this.updateToSchemaVersion(88);
}
// geo names indexes
if (databaseSchemaVersion < 89 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
await this.updateToSchemaVersion(89);
}
// hashrates indexes
if (databaseSchemaVersion < 90 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(90);
}
// block audits indexes
if (databaseSchemaVersion < 91 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
await this.updateToSchemaVersion(91);
}
// elements_pegs indexes
if (databaseSchemaVersion < 92 && config.MEMPOOL.NETWORK === 'liquid') {
await this.$executeQuery(`
ALTER TABLE \`elements_pegs\`
ADD INDEX \`block\` (\`block\`),
ADD INDEX \`datetime\` (\`datetime\`),
ADD INDEX \`amount\` (\`amount\`),
ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
`);
await this.updateToSchemaVersion(92);
}
// federation_txos indexes
if (databaseSchemaVersion < 93 && config.MEMPOOL.NETWORK === 'liquid') {
await this.$executeQuery(`
ALTER TABLE \`federation_txos\`
ADD INDEX \`unspent\` (\`unspent\`),
ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
ADD INDEX \`blocktime\` (\`blocktime\`),
ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
ADD INDEX \`expiredAt\` (\`expiredAt\`)
`);
await this.updateToSchemaVersion(93);
}
}
/**

View File

@@ -382,7 +382,7 @@ class MempoolBlocks {
const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = [];
let ancestor: MempoolTransactionExtended
let ancestor: MempoolTransactionExtended;
for (const cluster of clusters) {
for (const memberTxid of cluster) {
const mempoolTx = mempool[memberTxid];
@@ -462,7 +462,7 @@ class MempoolBlocks {
for (let i = 0; i < block.length; i++) {
const txid = block[i];
if (txid) {
if (txid in mempool) {
mempoolTx = mempool[txid];
// save position in projected blocks
mempoolTx.position = {
@@ -481,6 +481,9 @@ class MempoolBlocks {
mempoolTx.acceleratedAt = acceleration?.added;
mempoolTx.feeDelta = acceleration?.feeDelta;
for (const ancestor of mempoolTx.ancestors || []) {
if (!(ancestor.txid in mempool)) {
continue;
}
if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true;
}
@@ -688,7 +691,7 @@ class MempoolBlocks {
[pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean };
} = {};
// prepare a list of accelerations in ascending order (we'll pop items off the end of the list)
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).map(acc => {
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).filter(acc => acc.txid in mempoolCache).map(acc => {
let vsize = mempoolCache[acc.txid].vsize;
for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
vsize += (ancestor.weight / 4);

View File

@@ -10,6 +10,7 @@ import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache';
import { Acceleration } from './services/acceleration';
import accelerationApi from './services/acceleration';
import redisCache from './redis-cache';
import blocks from './blocks';
@@ -207,7 +208,7 @@ class Mempool {
return txTimes;
}
public async $updateMempool(transactions: string[], accelerations: Acceleration[] | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
public async $updateMempool(transactions: string[], accelerations: Record<string, Acceleration> | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
logger.debug(`Updating mempool...`);
// warn if this run stalls the main loop for more than 2 minutes
@@ -354,7 +355,7 @@ class Mempool {
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
const accelerationDelta = accelerations != null ? await this.$updateAccelerations(accelerations) : [];
const accelerationDelta = accelerations != null ? await this.updateAccelerations(accelerations) : [];
if (accelerationDelta.length) {
hasChange = true;
}
@@ -399,58 +400,11 @@ class Mempool {
return this.accelerations;
}
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
public updateAccelerations(newAccelerationMap: Record<string, Acceleration>): string[] {
try {
const changed: string[] = [];
const newAccelerationMap: { [txid: string]: Acceleration } = {};
for (const acceleration of newAccelerations) {
// skip transactions we don't know about
if (!this.mempoolCache[acceleration.txid]) {
continue;
}
newAccelerationMap[acceleration.txid] = acceleration;
if (this.accelerations[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else {
if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
} else if (this.accelerations[acceleration.txid].pools?.length) {
let poolsChanged = false;
const pools = new Set();
this.accelerations[acceleration.txid].pools.forEach(pool => {
pools.add(pool);
});
acceleration.pools.forEach(pool => {
if (!pools.has(pool)) {
poolsChanged = true;
} else {
pools.delete(pool);
}
});
if (pools.size > 0) {
poolsChanged = true;
}
if (poolsChanged) {
// pools changed
changed.push(acceleration.txid);
}
}
}
}
for (const oldTxid of Object.keys(this.accelerations)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
const accelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, newAccelerationMap);
this.accelerations = newAccelerationMap;
return changed;
return accelerationDelta;
} catch (e: any) {
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
return [];

View File

@@ -459,7 +459,7 @@ class MiningRoutes {
handleError(req, res, 400, 'Acceleration data is not available.');
return;
}
res.status(200).send(accelerationApi.accelerations || []);
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
} catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e);
}

View File

@@ -136,9 +136,13 @@ class Mining {
poolsStatistics['blockCount'] = blockCount;
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h');
const totalBlock3d: number = await BlocksRepository.$blockCount(null, '3d');
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
try {
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d);
poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w);
} catch (e) {
poolsStatistics['lastEstimatedHashrate'] = 0;
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);

View File

@@ -1,7 +1,10 @@
import { WebSocket } from 'ws';
import config from '../../config';
import logger from '../../logger';
import { BlockExtended } from '../../mempool.interfaces';
import axios from 'axios';
import mempool from '../mempool';
import websocketHandler from '../websocket-handler';
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
@@ -37,14 +40,23 @@ export interface AccelerationHistory {
};
class AccelerationApi {
private ws: WebSocket | null = null;
private useWebsocket: boolean = config.MEMPOOL.OFFICIAL && config.MEMPOOL_SERVICES.ACCELERATIONS;
private startedWebsocketLoop: boolean = false;
private websocketConnected: boolean = false;
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
private _accelerations: Acceleration[] | null = null;
private websocketPath = config.MEMPOOL_SERVICES?.API ? `${config.MEMPOOL_SERVICES.API.replace('https://', 'wss://').replace('http://', 'ws://')}/accelerator/ws` : '/';
private _accelerations: Record<string, Acceleration> = {};
private lastPoll = 0;
private lastPing = Date.now();
private lastPong = Date.now();
private forcePoll = false;
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
public get accelerations(): Acceleration[] | null {
public constructor() {}
public getAccelerations(): Record<string, Acceleration> {
return this._accelerations;
}
@@ -72,11 +84,18 @@ class AccelerationApi {
}
}
public async $updateAccelerations(): Promise<Acceleration[] | null> {
public async $updateAccelerations(): Promise<Record<string, Acceleration> | null> {
if (this.useWebsocket && this.websocketConnected) {
return this._accelerations;
}
if (!this.onDemandPollingEnabled) {
const accelerations = await this.$fetchAccelerations();
if (accelerations) {
this._accelerations = accelerations;
const latestAccelerations = {};
for (const acc of accelerations) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
return this._accelerations;
}
} else {
@@ -85,7 +104,7 @@ class AccelerationApi {
return null;
}
private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
private async $updateAccelerationsOnDemand(): Promise<Record<string, Acceleration> | null> {
const shouldUpdate = this.forcePoll
|| this.countMyAccelerationsWithStatus('requested') > 0
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
@@ -120,7 +139,11 @@ class AccelerationApi {
}
}
this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[];
const latestAccelerations = {};
for (const acc of Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
return this._accelerations;
}
@@ -152,6 +175,148 @@ class AccelerationApi {
}
return anyAccelerated;
}
// get a list of accelerations that have changed between two sets of accelerations
public getAccelerationDelta(oldAccelerationMap: Record<string, Acceleration>, newAccelerationMap: Record<string, Acceleration>): string[] {
const changed: string[] = [];
const mempoolCache = mempool.getMempool();
for (const acceleration of Object.values(newAccelerationMap)) {
// skip transactions we don't know about
if (!mempoolCache[acceleration.txid]) {
continue;
}
if (oldAccelerationMap[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else {
if (oldAccelerationMap[acceleration.txid].feeDelta !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
} else if (oldAccelerationMap[acceleration.txid].pools?.length) {
let poolsChanged = false;
const pools = new Set();
oldAccelerationMap[acceleration.txid].pools.forEach(pool => {
pools.add(pool);
});
acceleration.pools.forEach(pool => {
if (!pools.has(pool)) {
poolsChanged = true;
} else {
pools.delete(pool);
}
});
if (pools.size > 0) {
poolsChanged = true;
}
if (poolsChanged) {
// pools changed
changed.push(acceleration.txid);
}
}
}
}
for (const oldTxid of Object.keys(oldAccelerationMap)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
return changed;
}
private handleWebsocketMessage(msg: any): void {
if (msg?.accelerations !== null) {
const latestAccelerations = {};
for (const acc of msg?.accelerations || []) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
websocketHandler.handleAccelerationsChanged(this._accelerations);
}
}
public async connectWebsocket(): Promise<void> {
if (this.startedWebsocketLoop) {
return;
}
while (this.useWebsocket) {
this.startedWebsocketLoop = true;
if (!this.ws) {
this.ws = new WebSocket(this.websocketPath);
this.lastPing = 0;
this.ws.on('open', () => {
logger.info(`Acceleration websocket opened to ${this.websocketPath}`);
this.websocketConnected = true;
this.ws?.send(JSON.stringify({
'watch-accelerations': true
}));
});
this.ws.on('error', (error) => {
let errMsg = `Acceleration websocket error on ${this.websocketPath}: ${error['code']}`;
if (error['errors']) {
errMsg += ' - ' + error['errors'].join(' - ');
}
logger.err(errMsg);
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('close', () => {
logger.info('Acceleration websocket closed');
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('message', (data, isBinary) => {
try {
const msg = (isBinary ? data : data.toString()) as string;
const parsedMsg = msg?.length ? JSON.parse(msg) : null;
this.handleWebsocketMessage(parsedMsg);
} catch (e) {
logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e));
}
});
this.ws.on('ping', () => {
logger.debug('received ping from acceleration websocket server');
});
this.ws.on('pong', () => {
logger.debug('received pong from acceleration websocket server');
this.lastPong = Date.now();
});
} else if (this.websocketConnected) {
if (this.lastPing && this.lastPing > this.lastPong && (Date.now() - this.lastPing > 10000)) {
logger.warn('No pong received within 10 seconds, terminating connection');
try {
this.ws?.terminate();
} catch (e) {
logger.warn('failed to terminate acceleration websocket connection: ' + (e instanceof Error ? e.message : e));
} finally {
this.ws = null;
this.websocketConnected = false;
this.lastPing = 0;
}
} else if (!this.lastPing || (Date.now() - this.lastPing > 30000)) {
logger.debug('sending ping to acceleration websocket server');
if (this.ws?.readyState === WebSocket.OPEN) {
try {
this.ws?.ping();
this.lastPing = Date.now();
} catch (e) {
logger.warn('failed to send ping to acceleration websocket server: ' + (e instanceof Error ? e.message : e));
}
}
}
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
}
export default new AccelerationApi();

View File

@@ -0,0 +1,26 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import WalletApi from './wallets';
class ServicesRoutes {
public initRoutes(app: Application): void {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'wallet/:walletId', this.$getWallet)
;
}
private async $getWallet(req: Request, res: Response): Promise<void> {
try {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 5).toUTCString());
const walletId = req.params.walletId;
const wallet = await WalletApi.getWallet(walletId);
res.status(200).send(wallet);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
export default new ServicesRoutes();

View File

@@ -0,0 +1,153 @@
import config from '../../config';
import logger from '../../logger';
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
import bitcoinApi from '../bitcoin/bitcoin-api-factory';
import axios from 'axios';
import { TransactionExtended } from '../../mempool.interfaces';
interface WalletAddress {
address: string;
active: boolean;
stats: {
funded_txo_count: number;
funded_txo_sum: number;
spent_txo_count: number;
spent_txo_sum: number;
tx_count: number;
};
transactions: IEsploraApi.AddressTxSummary[];
lastSync: number;
}
interface Wallet {
name: string;
addresses: Record<string, WalletAddress>;
lastPoll: number;
}
const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes
class WalletApi {
private wallets: Record<string, Wallet> = {};
private syncing = false;
constructor() {
this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => {
acc[wallet] = { name: wallet, addresses: {}, lastPoll: 0 };
return acc;
}, {} as Record<string, Wallet>) : {};
}
public getWallet(wallet: string): Record<string, WalletAddress> {
return this.wallets?.[wallet]?.addresses || {};
}
// resync wallet addresses from the services backend
async $syncWallets(): Promise<void> {
if (!config.WALLETS.ENABLED || this.syncing) {
return;
}
this.syncing = true;
for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey];
if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) {
try {
const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets/${wallet.name}`);
const addresses: Record<string, WalletAddress> = response.data;
const addressList: WalletAddress[] = Object.values(addresses);
// sync all current addresses
for (const address of addressList) {
await this.$syncWalletAddress(wallet, address);
}
// remove old addresses
for (const address of Object.keys(wallet.addresses)) {
if (!addresses[address]) {
delete wallet.addresses[address];
}
}
wallet.lastPoll = Date.now();
logger.debug(`Synced ${Object.keys(wallet.addresses).length} addresses for wallet ${wallet.name}`);
} catch (e) {
logger.err(`Error syncing wallet ${wallet.name}: ${(e instanceof Error ? e.message : e)}`);
}
}
}
this.syncing = false;
}
// resync address transactions from esplora
async $syncWalletAddress(wallet: Wallet, address: WalletAddress): Promise<void> {
// fetch full transaction data if the address is new or still active and hasn't been synced in the last hour
const refreshTransactions = !wallet.addresses[address.address] || (address.active && (Date.now() - wallet.addresses[address.address].lastSync) > 60 * 60 * 1000);
if (refreshTransactions) {
try {
const summary = await bitcoinApi.$getAddressTransactionSummary(address.address);
const addressInfo = await bitcoinApi.$getAddress(address.address);
const walletAddress: WalletAddress = {
address: address.address,
active: address.active,
transactions: summary,
stats: addressInfo.chain_stats,
lastSync: Date.now(),
};
wallet.addresses[address.address] = walletAddress;
} catch (e) {
logger.err(`Error syncing wallet address ${address.address}: ${(e instanceof Error ? e.message : e)}`);
}
}
}
// check a new block for transactions that affect wallet address balances, and add relevant transactions to wallets
processBlock(block: IEsploraApi.Block, blockTxs: TransactionExtended[]): Record<string, IEsploraApi.Transaction[]> {
const walletTransactions: Record<string, IEsploraApi.Transaction[]> = {};
for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey];
walletTransactions[walletKey] = [];
for (const tx of blockTxs) {
const funded: Record<string, number> = {};
const spent: Record<string, number> = {};
const fundedCount: Record<string, number> = {};
const spentCount: Record<string, number> = {};
let anyMatch = false;
for (const vin of tx.vin) {
const address = vin.prevout?.scriptpubkey_address;
if (address && wallet.addresses[address]) {
anyMatch = true;
spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0);
spentCount[address] = (spentCount[address] ?? 0) + 1;
}
}
for (const vout of tx.vout) {
const address = vout.scriptpubkey_address;
if (address && wallet.addresses[address]) {
anyMatch = true;
funded[address] = (funded[address] ?? 0) + (vout.value ?? 0);
fundedCount[address] = (fundedCount[address] ?? 0) + 1;
}
}
for (const address of Object.keys({ ...funded, ...spent })) {
// update address stats
wallet.addresses[address].stats.tx_count++;
wallet.addresses[address].stats.funded_txo_count += fundedCount[address] || 0;
wallet.addresses[address].stats.spent_txo_count += spentCount[address] || 0;
wallet.addresses[address].stats.funded_txo_sum += funded[address] || 0;
wallet.addresses[address].stats.spent_txo_sum += spent[address] || 0;
// add tx to summary
const txSummary: IEsploraApi.AddressTxSummary = {
txid: tx.txid,
value: (funded[address] ?? 0) - (spent[address] ?? 0),
height: block.height,
time: block.timestamp,
};
wallet.addresses[address].transactions?.push(txSummary);
}
if (anyMatch) {
walletTransactions[walletKey].push(tx);
}
}
}
return walletTransactions;
}
}
export default new WalletApi();

View File

@@ -22,11 +22,13 @@ import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository
import Audit from './audit';
import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository';
import { Acceleration } from './services/acceleration';
import accelerationApi from './services/acceleration';
import mempool from './mempool';
import statistics from './statistics/statistics';
import accelerationRepository from '../repositories/AccelerationRepository';
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import walletApi from './services/wallets';
interface AddressTransactions {
mempool: MempoolTransactionExtended[],
@@ -59,6 +61,8 @@ class WebsocketHandler {
private lastRbfSummary: ReplacementInfo[] | null = null;
private mempoolSequence: number = 0;
private accelerations: Record<string, Acceleration> = {};
constructor() { }
addWebsocketServer(wss: WebSocket.Server) {
@@ -307,6 +311,14 @@ class WebsocketHandler {
}
}
if (parsedMessage && parsedMessage['track-wallet']) {
if (parsedMessage['track-wallet'] === 'stop') {
client['track-wallet'] = null;
} else {
client['track-wallet'] = parsedMessage['track-wallet'];
}
}
if (parsedMessage && parsedMessage['track-asset']) {
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) {
client['track-asset'] = parsedMessage['track-asset'];
@@ -486,6 +498,42 @@ class WebsocketHandler {
}
}
handleAccelerationsChanged(accelerations: Record<string, Acceleration>): void {
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server has been set');
}
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
this.accelerations = accelerations;
if (!websocketAccelerationDelta.length) {
return;
}
// pre-compute acceleration delta
const accelerationUpdate = {
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
};
try {
const response = JSON.stringify({
accelerations: accelerationUpdate,
});
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
client.send(response);
});
}
} catch (e) {
logger.debug(`Error sending acceleration update to websocket clients: ${e}`);
}
}
handleReorg(): void {
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set');
@@ -562,7 +610,7 @@ class WebsocketHandler {
const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
const da = difficultyAdjustment.getDifficultyAdjustment();
const accelerations = memPool.getAccelerations();
const accelerations = accelerationApi.getAccelerations();
memPool.handleRbfTransactions(rbfTransactions);
const rbfChanges = rbfCache.getRbfChanges();
let rbfReplacements;
@@ -670,10 +718,13 @@ class WebsocketHandler {
const addressCache = this.makeAddressCache(newTransactions);
const removedAddressCache = this.makeAddressCache(deletedTransactions);
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
this.accelerations = accelerations;
// pre-compute acceleration delta
const accelerationUpdate = {
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: accelerationDelta.filter(txid => !accelerations[txid]),
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
};
// TODO - Fix indentation after PR is merged
@@ -1112,6 +1163,9 @@ class WebsocketHandler {
replaced: replacedTransactions,
};
// check for wallet transactions
const walletTransactions = config.WALLETS.ENABLED ? walletApi.processBlock(block, transactions) : [];
const responseCache = { ...this.socketData };
function getCachedResponse(key, data): string {
if (!responseCache[key]) {
@@ -1316,6 +1370,11 @@ class WebsocketHandler {
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
}
if (client['track-wallet']) {
const trackedWallet = client['track-wallet'];
response['wallet-transactions'] = getCachedResponse(`wallet-transactions-${trackedWallet}`, walletTransactions[trackedWallet] ?? {});
}
if (Object.keys(response).length) {
client.send(this.serializeResponse(response));
}

View File

@@ -162,6 +162,10 @@ interface IConfig {
PAID: boolean;
API_KEY: string;
},
WALLETS: {
ENABLED: boolean;
WALLETS: string[];
}
}
const defaults: IConfig = {
@@ -324,6 +328,10 @@ const defaults: IConfig = {
'PAID': false,
'API_KEY': '',
},
'WALLETS': {
'ENABLED': false,
'WALLETS': [],
},
};
class Config implements IConfig {
@@ -345,6 +353,7 @@ class Config implements IConfig {
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
REDIS: IConfig['REDIS'];
FIAT_PRICE: IConfig['FIAT_PRICE'];
WALLETS: IConfig['WALLETS'];
constructor() {
const configs = this.merge(configFromFile, defaults);
@@ -366,6 +375,7 @@ class Config implements IConfig {
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
this.REDIS = configs.REDIS;
this.FIAT_PRICE = configs.FIAT_PRICE;
this.WALLETS = configs.WALLETS;
}
merge = (...objects: object[]): IConfig => {

View File

@@ -32,6 +32,7 @@ import pricesRoutes from './api/prices/prices.routes';
import miningRoutes from './api/mining/mining-routes';
import liquidRoutes from './api/liquid/liquid.routes';
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
import servicesRoutes from './api/services/services-routes';
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
import forensicsService from './tasks/lightning/forensics.service';
import priceUpdater from './tasks/price-updater';
@@ -46,6 +47,7 @@ import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes';
import mempoolBlocks from './api/mempool-blocks';
import walletApi from './api/services/wallets';
class Server {
private wss: WebSocket.Server | undefined;
@@ -231,13 +233,17 @@ class Server {
const newMempool = await bitcoinApi.$getRawMempool();
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
const newAccelerations = await accelerationApi.$updateAccelerations();
const latestAccelerations = await accelerationApi.$updateAccelerations();
const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate);
await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate);
}
indexer.$run();
if (config.WALLETS.ENABLED) {
// might take a while, so run in the background
walletApi.$syncWallets();
}
if (config.FIAT_PRICE.ENABLED) {
priceUpdater.$run();
}
@@ -312,8 +318,10 @@ class Server {
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
}
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
accelerationApi.connectWebsocket();
}
setUpHttpApiRoutes(): void {
bitcoinRoutes.initRoutes(this.app);
bitcoinCoreRoutes.initRoutes(this.app);
@@ -335,6 +343,9 @@ class Server {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
accelerationRoutes.initRoutes(this.app);
}
if (config.WALLETS.ENABLED) {
servicesRoutes.initRoutes(this.app);
}
if (!config.MEMPOOL.OFFICIAL) {
aboutRoutes.initRoutes(this.app);
}

View File

@@ -501,7 +501,7 @@ class BlocksRepository {
}
query += ` ORDER BY height DESC
LIMIT 10`;
LIMIT 100`;
try {
const [rows]: any[] = await DB.query(query, params);

View File

@@ -0,0 +1,48 @@
{
"theme": "wiz",
"enterprise": "bitb",
"branding": {
"name": "bitb",
"title": "BITB",
"site_id": 20,
"header_img": "/resources/bitblogo.svg",
"footer_img": "/resources/bitblogo.svg"
},
"dashboard": {
"widgets": [
{
"component": "fees",
"mobileOrder": 4
},
{
"component": "walletBalance",
"mobileOrder": 1,
"props": {
"wallet": "BITB"
}
},
{
"component": "goggles",
"mobileOrder": 5
},
{
"component": "wallet",
"mobileOrder": 2,
"props": {
"wallet": "BITB",
"period": "all"
}
},
{
"component": "blocks"
},
{
"component": "walletTransactions",
"mobileOrder": 3,
"props": {
"wallet": "BITB"
}
}
]
}
}

View File

@@ -0,0 +1,51 @@
{
"theme": "contrast",
"enterprise": "meta",
"branding": {
"name": "metaplanet",
"title": "Metaplanet",
"site_id": 21,
"header_img": "/resources/metalogo.svg",
"footer_img": "/resources/metalogo.svg"
},
"dashboard": {
"widgets": [
{
"component": "fees",
"mobileOrder": 4
},
{
"component": "walletBalance",
"mobileOrder": 1,
"props": {
"wallet": "3350"
}
},
{
"component": "twitter",
"mobileOrder": 5,
"props": {
"handle": "Metaplanet_JP"
}
},
{
"component": "wallet",
"mobileOrder": 2,
"props": {
"wallet": "3350",
"period": "all"
}
},
{
"component": "blocks"
},
{
"component": "walletTransactions",
"mobileOrder": 3,
"props": {
"wallet": "3350"
}
}
]
}
}

View File

@@ -1,15 +1,15 @@
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';
import { AddressGroupComponent } from './components/address-group/address-group.component';
import { TrackerComponent } from './components/tracker/tracker.component';
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component';
import { TrackerGuard } from './route-guards';
import { AppPreloadingStrategy } from '@app/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';
import { AddressGroupComponent } from '@components/address-group/address-group.component';
import { TrackerComponent } from '@components/tracker/tracker.component';
import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component';
import { TrackerGuard } from '@app/route-guards';
const browserWindow = window || {};
// @ts-ignore
@@ -22,16 +22,16 @@ let routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
path: 'widget/wallet',
children: [],
component: AddressGroupComponent,
data: {
@@ -45,7 +45,7 @@ let routes: Routes = [
},
{
path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
@@ -60,12 +60,12 @@ let routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
@@ -83,7 +83,7 @@ let routes: Routes = [
},
{
path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
@@ -103,16 +103,16 @@ let routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
path: 'widget/wallet',
children: [],
component: AddressGroupComponent,
data: {
@@ -126,7 +126,7 @@ let routes: Routes = [
},
{
path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
@@ -138,22 +138,22 @@ let routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
{
path: 'tx',
canMatch: [TrackerGuard],
runGuardsAndResolvers: 'always',
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
loadChildren: () => import('@components/tracker/tracker.module').then(m => m.TrackerModule),
},
{
path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
path: 'widget/wallet',
children: [],
component: AddressGroupComponent,
data: {
@@ -165,19 +165,19 @@ let routes: Routes = [
children: [
{
path: '',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
},
{
path: 'testnet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
},
{
path: 'testnet4',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
},
{
path: 'signet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
},
],
},
@@ -212,7 +212,7 @@ let routes: Routes = [
},
{
path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true },
},
];
@@ -225,16 +225,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
path: 'widget/wallet',
children: [],
component: AddressGroupComponent,
data: {
@@ -248,7 +248,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
},
{
path: '',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
@@ -260,16 +260,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
{
path: '',
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true },
},
{
path: 'wallet',
path: 'widget/wallet',
children: [],
component: AddressGroupComponent,
data: {
@@ -281,11 +281,11 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
children: [
{
path: '',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
},
{
path: 'testnet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
},
],
},
@@ -296,7 +296,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
},
{
path: '',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true },
},
];

View File

@@ -2,11 +2,11 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { ZONE_SERVICE } from './injection-tokens';
import { ZONE_SERVICE } from '@app/injection-tokens';
import { AppModule } from './app.module';
import { AppComponent } from './components/app/app.component';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { ZoneService } from './services/zone.service';
import { AppComponent } from '@components/app/app.component';
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
import { ZoneService } from '@app/services/zone.service';
@NgModule({
@@ -20,4 +20,4 @@ import { ZoneService } from './services/zone.service';
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
export class AppServerModule {}

View File

@@ -2,33 +2,33 @@ import { BrowserModule } from '@angular/platform-browser';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ZONE_SERVICE } from './injection-tokens';
import { ZONE_SERVICE } from '@app/injection-tokens';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './components/app/app.component';
import { ElectrsApiService } from './services/electrs-api.service';
import { OrdApiService } from './services/ord-api.service';
import { StateService } from './services/state.service';
import { CacheService } from './services/cache.service';
import { PriceService } from './services/price.service';
import { EnterpriseService } from './services/enterprise.service';
import { WebsocketService } from './services/websocket.service';
import { AudioService } from './services/audio.service';
import { PreloadService } from './services/preload.service';
import { SeoService } from './services/seo.service';
import { OpenGraphService } from './services/opengraph.service';
import { ZoneService } from './services/zone-shim.service';
import { SharedModule } from './shared/shared.module';
import { StorageService } from './services/storage.service';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { LanguageService } from './services/language.service';
import { ThemeService } from './services/theme.service';
import { TimeService } from './services/time.service';
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
import { AppPreloadingStrategy } from './app.preloading-strategy';
import { ServicesApiServices } from './services/services-api.service';
import { AppComponent } from '@components/app/app.component';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { OrdApiService } from '@app/services/ord-api.service';
import { StateService } from '@app/services/state.service';
import { CacheService } from '@app/services/cache.service';
import { PriceService } from '@app/services/price.service';
import { EnterpriseService } from '@app/services/enterprise.service';
import { WebsocketService } from '@app/services/websocket.service';
import { AudioService } from '@app/services/audio.service';
import { PreloadService } from '@app/services/preload.service';
import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { ZoneService } from '@app/services/zone-shim.service';
import { SharedModule } from '@app/shared/shared.module';
import { StorageService } from '@app/services/storage.service';
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
import { LanguageService } from '@app/services/language.service';
import { ThemeService } from '@app/services/theme.service';
import { TimeService } from '@app/services/time.service';
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { CapAddressPipe } from '@app/shared/pipes/cap-address-pipe/cap-address-pipe';
import { AppPreloadingStrategy } from '@app/app.preloading-strategy';
import { ServicesApiServices } from '@app/services/services-api.service';
import { DatePipe } from '@angular/common';
const providers = [

View File

@@ -1,13 +1,13 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { MasterPageComponent } from './components/master-page/master-page.component';
import { MasterPageComponent } from '@components/master-page/master-page.component';
const routes: Routes = [
{
path: '',
component: MasterPageComponent,
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
loadChildren: () => import('@app/graphs/graphs.module').then(m => m.GraphsModule),
data: { preload: true },
}
];

View File

@@ -1,5 +1,5 @@
import { Transaction, Vin } from './interfaces/electrs.interface';
import { Hash } from './shared/sha256';
import { Transaction, Vin } from '@interfaces/electrs.interface';
import { Hash } from '@app/shared/sha256';
const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH
const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH
@@ -303,4 +303,4 @@ export async function calcScriptHash$(script: string): Promise<string> {
return hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0'))
.join('');
}
}

View File

@@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core';
import { EnterpriseService } from '../../services/enterprise.service';
import { EnterpriseService } from '@app/services/enterprise.service';
@Component({
selector: 'app-about-sponsors',

View File

@@ -1,16 +1,16 @@
import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core';
import { WebsocketService } from '../../services/websocket.service';
import { SeoService } from '../../services/seo.service';
import { OpenGraphService } from '../../services/opengraph.service';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '@app/services/websocket.service';
import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { StateService } from '@app/services/state.service';
import { Observable } from 'rxjs';
import { ApiService } from '../../services/api.service';
import { IBackendInfo } from '../../interfaces/websocket.interface';
import { ApiService } from '@app/services/api.service';
import { IBackendInfo } from '@interfaces/websocket.interface';
import { Router, ActivatedRoute } from '@angular/router';
import { map, share, tap } from 'rxjs/operators';
import { ITranslators } from '../../interfaces/node-api.interface';
import { ITranslators } from '@interfaces/node-api.interface';
import { DOCUMENT } from '@angular/common';
import { EnterpriseService } from '../../services/enterprise.service';
import { EnterpriseService } from '@app/services/enterprise.service';
@Component({
selector: 'app-about',

View File

@@ -1,9 +1,9 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from './about.component';
import { AboutSponsorsComponent } from './about-sponsors.component';
import { SharedModule } from '../../shared/shared.module';
import { AboutComponent } from '@components/about/about.component';
import { AboutSponsorsComponent } from '@components/about/about-sponsors.component';
import { SharedModule } from '@app/shared/shared.module';
const routes: Routes = [
{

View File

@@ -172,10 +172,6 @@
background-color: var(--tertiary);
}
.btn-small-height {
line-height: 1;
}
.summary-row {
display: flex;
flex-direction: row;

View File

@@ -1,16 +1,16 @@
/* eslint-disable no-console */
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
import { ServicesApiServices } from '../../services/services-api.service';
import { md5, insecureRandomUUID } from '../../shared/common.utils';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
import { ETA, EtaService } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { MiningStats } from '../../services/mining.service';
import { IAuth, AuthServiceMempool } from '../../services/auth.service';
import { EnterpriseService } from '../../services/enterprise.service';
import { ApiService } from '../../services/api.service';
import { ServicesApiServices } from '@app/services/services-api.service';
import { md5 } from '@app/shared/common.utils';
import { StateService } from '@app/services/state.service';
import { AudioService } from '@app/services/audio.service';
import { ETA, EtaService } from '@app/services/eta.service';
import { Transaction } from '@interfaces/electrs.interface';
import { MiningStats } from '@app/services/mining.service';
import { IAuth, AuthServiceMempool } from '@app/services/auth.service';
import { EnterpriseService } from '@app/services/enterprise.service';
import { ApiService } from '@app/services/api.service';
import { isDevMode } from '@angular/core';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
@@ -84,13 +84,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
timePaid: number = 0; // time acceleration requested
math = Math;
isMobile: boolean = window.innerWidth <= 767.98;
isProdDomain = ['mempool.space',
'mempool-staging.va1.mempool.space',
'mempool-staging.fmt.mempool.space',
'mempool-staging.fra.mempool.space',
'mempool-staging.tk7.mempool.space',
'mempool-staging.sg1.mempool.space'
].indexOf(document.location.hostname) > -1;
isProdDomain = false;
private _step: CheckoutStep = 'summary';
simpleMode: boolean = true;
@@ -100,7 +94,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
auth: IAuth | null = null;
// accelerator stuff
accelerationUUID: string;
accelerationSubscription: Subscription;
difficultySubscription: Subscription;
estimateSubscription: Subscription;
@@ -143,7 +136,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
private authService: AuthServiceMempool,
private enterpriseService: EnterpriseService,
) {
this.accelerationUUID = insecureRandomUUID();
this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1;
// Check if Apple Pay available
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
@@ -207,6 +200,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
}
moveToStep(step: CheckoutStep): void {
this.processing = false;
this._step = step;
if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer);
@@ -392,7 +386,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerationSubscription = this.servicesApiService.accelerate$(
this.tx.txid,
this.userBid,
this.accelerationUUID
).subscribe({
next: () => {
this.processing = false;
@@ -526,7 +519,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID,
costUSD
).subscribe({
next: () => {
@@ -626,7 +618,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token,
cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID,
costUSD
).subscribe({
next: () => {
@@ -717,7 +708,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token,
tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId,
this.accelerationUUID,
costUSD
).subscribe({
next: () => {

View File

@@ -1,6 +1,6 @@
import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { Transaction } from '../../interfaces/electrs.interface';
import { AccelerationEstimate, RateOption } from './accelerate-checkout.component';
import { Transaction } from '@interfaces/electrs.interface';
import { AccelerationEstimate, RateOption } from '@components/accelerate-checkout/accelerate-checkout.component';
interface GraphBar {
rate: number;

View File

@@ -1,8 +1,8 @@
import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core';
import { ETA } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '../../interfaces/node-api.interface';
import { MiningService } from '../../services/mining.service';
import { ETA } from '@app/services/eta.service';
import { Transaction } from '@interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
import { MiningService } from '@app/services/mining.service';
@Component({
selector: 'app-acceleration-timeline',

View File

@@ -1,18 +1,18 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { EChartsOption } from '../../../graphs/echarts';
import { EChartsOption } from '@app/graphs/echarts';
import { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs';
import { startWith, switchMap, tap } from 'rxjs/operators';
import { SeoService } from '../../../services/seo.service';
import { SeoService } from '@app/services/seo.service';
import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../../shared/graphs.utils';
import { StorageService } from '../../../services/storage.service';
import { MiningService } from '../../../services/mining.service';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '@app/shared/graphs.utils';
import { StorageService } from '@app/services/storage.service';
import { MiningService } from '@app/services/mining.service';
import { ActivatedRoute, Router } from '@angular/router';
import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service';
import { StateService } from '../../../services/state.service';
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe';
import { Acceleration } from '@interfaces/node-api.interface';
import { ServicesApiServices } from '@app/services/services-api.service';
import { StateService } from '@app/services/state.service';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
@Component({
selector: 'app-acceleration-fees-graph',

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { ServicesApiServices } from '../../../services/services-api.service';
import { ServicesApiServices } from '@app/services/services-api.service';
export type AccelerationStats = {
totalRequested: number;

View File

@@ -4,7 +4,7 @@
<div class="clearfix"></div>
<div class="acceleration-list">
<div class="acceleration-list" *ngIf="{ accelerations: accelerationList$ | async } as state">
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
<thead>
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
@@ -21,8 +21,8 @@
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
</ng-container>
</thead>
<tbody *ngIf="accelerationList$ | async as accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let acceleration of accelerations; let i= index;">
<tbody *ngIf="state.accelerations && nonEmptyAccelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let acceleration of state.accelerations; let i= index;">
<td class="txid text-left">
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
<app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate>

View File

@@ -1,12 +1,12 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core';
import { BehaviorSubject, Observable, Subscription, catchError, combineLatest, filter, of, switchMap, tap, throttleTime, timer } from 'rxjs';
import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
import { ServicesApiServices } from '../../../services/services-api.service';
import { SeoService } from '../../../services/seo.service';
import { Acceleration, BlockExtended, SinglePoolStats } from '@interfaces/node-api.interface';
import { StateService } from '@app/services/state.service';
import { WebsocketService } from '@app/services/websocket.service';
import { ServicesApiServices } from '@app/services/services-api.service';
import { SeoService } from '@app/services/seo.service';
import { ActivatedRoute, Router } from '@angular/router';
import { MiningService } from '../../../services/mining.service';
import { MiningService } from '@app/services/mining.service';
@Component({
selector: 'app-accelerations-list',
@@ -151,4 +151,4 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
this.paramSubscription?.unsubscribe();
this.keyNavigationSubscription?.unsubscribe();
}
}
}

View File

@@ -1,18 +1,18 @@
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { SeoService } from '../../../services/seo.service';
import { OpenGraphService } from '../../../services/opengraph.service';
import { WebsocketService } from '../../../services/websocket.service';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { WebsocketService } from '@app/services/websocket.service';
import { Acceleration, BlockExtended } from '@interfaces/node-api.interface';
import { StateService } from '@app/services/state.service';
import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs';
import { Color } from '../../block-overview-graph/sprite-types';
import { hexToColor } from '../../block-overview-graph/utils';
import TxView from '../../block-overview-graph/tx-view';
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../../app.constants';
import { ServicesApiServices } from '../../../services/services-api.service';
import { detectWebGL } from '../../../shared/graphs.utils';
import { AudioService } from '../../../services/audio.service';
import { ThemeService } from '../../../services/theme.service';
import { Color } from '@components/block-overview-graph/sprite-types';
import { hexToColor } from '@components/block-overview-graph/utils';
import TxView from '@components/block-overview-graph/tx-view';
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '@app/app.constants';
import { ServicesApiServices } from '@app/services/services-api.service';
import { detectWebGL } from '@app/shared/graphs.utils';
import { AudioService } from '@app/services/audio.service';
import { ThemeService } from '@app/services/theme.service';
const acceleratedColor: Color = hexToColor('8F5FF6');
const normalColors = defaultMempoolFeeColors.map(hex => hexToColor(hex + '5F'));

View File

@@ -20,7 +20,7 @@
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
<div class="d-flex justify-content-between align-items-start">
@if (hasCpfp) {
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
}
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
</div>
@@ -36,7 +36,7 @@
<tr>
<td colspan="3" class="pt-0">
<div class="d-flex justify-content-end align-items-start">
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
</div>
</td>
</tr>

View File

@@ -1,8 +1,8 @@
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter, ChangeDetectorRef } from '@angular/core';
import { Transaction } from '../../../interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface';
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
import { MiningStats } from '../../../services/mining.service';
import { Transaction } from '@interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
import { EChartsOption, PieSeriesOption } from '@app/graphs/echarts';
import { MiningStats } from '@app/services/mining.service';
function lighten(color, p): { r, g, b } {
return {
@@ -76,15 +76,21 @@ export class ActiveAccelerationBox implements OnChanges {
acceleratingPools.forEach((poolId, index) => {
const pool = pools[poolId];
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
let color = 'white';
if (index >= firstSignificantPool) {
if (numSignificantPools > 1) {
color = toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / Math.max((numSignificantPools - 1), 1)));
} else {
color = toRGB({ r: 147, g: 57, b: 244 });
}
}
data.push(getDataItem(
pool.lastEstimatedHashrate,
index >= firstSignificantPool
? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1)))
: 'white',
color,
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
true,
) as PieSeriesOption);
})
});
this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%';
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
data.push(getDataItem(
@@ -148,4 +154,4 @@ export class ActiveAccelerationBox implements OnChanges {
onToggleCpfp(): void {
this.toggleCpfp.emit();
}
}
}

View File

@@ -1,9 +1,9 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { Acceleration } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service';
import { Acceleration } from '@interfaces/node-api.interface';
import { StateService } from '@app/services/state.service';
import { WebsocketService } from '@app/services/websocket.service';
@Component({
selector: 'app-pending-stats',

View File

@@ -1,16 +1,15 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { echarts, EChartsOption } from '@app/graphs/echarts';
import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
import { AddressTxSummary, ChainStats } from '@interfaces/electrs.interface';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
import { Router } from '@angular/router';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '../../services/state.service';
import { PriceService } from '../../services/price.service';
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '@app/services/state.service';
import { PriceService } from '@app/services/price.service';
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
const periodSeconds = {
'1d': (60 * 60 * 24),
@@ -77,13 +76,12 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
private relativeUrlPipe: RelativeUrlPipe,
private priceService: PriceService,
private fiatCurrencyPipe: FiatCurrencyPipe,
private fiatShortenerPipe: FiatShortenerPipe,
private zone: NgZone,
) {}
ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true;
if (!this.address || !this.stats) {
if (!this.addressSummary$ && (!this.address || !this.stats)) {
return;
}
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
@@ -144,15 +142,16 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
}
prepareChartOptions(summary: AddressTxSummary[]) {
if (!summary || !this.stats) {
if (!summary) {
return;
}
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum);
const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0);
let runningTotal = total;
const processData = summary.map(d => {
const balance = total;
const fiatBalance = total * d.price / 100_000_000;
total -= d.value;
const balance = runningTotal;
const fiatBalance = runningTotal * d.price / 100_000_000;
runningTotal -= d.value;
return {
time: d.time * 1000,
balance,
@@ -172,7 +171,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
this.fiatData = this.fiatData.filter(d => d[0] >= startFiat);
}
this.data.push(
{value: [now, this.stats.funded_txo_sum - this.stats.spent_txo_sum], symbol: 'none', tooltip: { show: false }}
{value: [now, total], symbol: 'none', tooltip: { show: false }}
);
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);
@@ -244,18 +243,19 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
let tooltip = '<div>';
const hasTx = data[0].data[2].txid;
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
tooltip += `<div>
<div style="text-align: right;">
<div><b>${date}</b></div>`;
if (hasTx) {
const header = data.length === 1
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
: `${data.length} transactions`;
tooltip += `<span><b>${header}</b></span>`;
tooltip += `<div><b>${header}</b></div>`;
}
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
tooltip += `<div>
<div style="text-align: right;">`;
const formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal);
const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD');
@@ -290,7 +290,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
}
}
tooltip += `</div><span>${date}</span></div>`;
tooltip += `</div></div>`;
return tooltip;
}.bind(this)
},
@@ -310,18 +310,21 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
formatter: (val): string => {
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
if (valSpan > 100_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0)} BTC`;
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0, undefined, true, true)} BTC`;
}
else if (valSpan > 1_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2)} BTC`;
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2, undefined, true, true)} BTC`;
} else if (valSpan > 100_000_000) {
return `${(val / 100_000_000).toFixed(1)} BTC`;
} else if (valSpan > 10_000_000) {
return `${(val / 100_000_000).toFixed(2)} BTC`;
} else if (valSpan > 1_000_000) {
if (maxValue > 100_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 3, undefined, true, true)} BTC`;
}
return `${(val / 100_000_000).toFixed(3)} BTC`;
} else {
return `${this.amountShortenerPipe.transform(val, 0)} sats`;
return `${this.amountShortenerPipe.transform(val, 0, undefined, true, true)} sats`;
}
}
},
@@ -335,7 +338,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: function(val) {
return this.fiatShortenerPipe.transform(val, null, 'USD');
return `$${this.amountShortenerPipe.transform(val, 0, undefined, true, true)}`;
}.bind(this)
},
splitLine: {

View File

@@ -1,15 +1,15 @@
import { Component, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, catchError } from 'rxjs/operators';
import { Address, Transaction } from '../../interfaces/electrs.interface';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { Address, Transaction } from '@interfaces/electrs.interface';
import { WebsocketService } from '@app/services/websocket.service';
import { StateService } from '@app/services/state.service';
import { AudioService } from '@app/services/audio.service';
import { ApiService } from '@app/services/api.service';
import { of, Subscription, forkJoin } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { AddressInformation } from '../../interfaces/node-api.interface';
import { SeoService } from '@app/services/seo.service';
import { AddressInformation } from '@interfaces/node-api.interface';
@Component({
selector: 'app-address-group',

View File

@@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
import { Vin, Vout } from '../../interfaces/electrs.interface';
import { StateService } from '../../services/state.service';
import { AddressType, AddressTypeInfo } from '../../shared/address-utils';
import { Vin, Vout } from '@interfaces/electrs.interface';
import { StateService } from '@app/services/state.service';
import { AddressType, AddressTypeInfo } from '@app/shared/address-utils';
@Component({
selector: 'app-address-labels',

View File

@@ -12,7 +12,7 @@
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
</a>
</td>
<td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount></td>
<td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" [digitsInfo]="getAmountDigits(transaction.value)" [noFiat]="true"></app-amount></td>
<td class="table-cell-fiat" ><app-fiat [value]="transaction.value" [blockConversion]="transaction.price" digitsInfo="1.0-0"></app-fiat></td>
<td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true" [showTooltip]="true"></app-time></td>
</tr>

View File

@@ -1,9 +1,9 @@
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { StateService } from '@app/services/state.service';
import { Address, AddressTxSummary } from '@interfaces/electrs.interface';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { Observable, Subscription, catchError, map, of, switchMap, zip } from 'rxjs';
import { PriceService } from '../../services/price.service';
import { PriceService } from '@app/services/price.service';
@Component({
selector: 'app-address-transactions-widget',
@@ -43,7 +43,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
startAddressSubscription(): void {
this.isLoading = true;
if (!this.address || !this.addressInfo) {
if (!this.addressSummary$ && (!this.address || !this.addressInfo)) {
return;
}
this.transactions$ = (this.addressSummary$ || (this.isPubkey
@@ -55,7 +55,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
})
)).pipe(
map(summary => {
return summary?.slice(0, 6);
return summary?.filter(tx => Math.abs(tx.value) >= 1000000)?.slice(0, 6);
}),
switchMap(txs => {
return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe(
@@ -68,6 +68,12 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
))));
})
);
}
getAmountDigits(value: number): string {
const decimals = Math.max(0, 4 - Math.ceil(Math.log10(Math.abs(value / 100_000_000))));
return `1.${decimals}-${decimals}`;
}
ngOnDestroy(): void {

View File

@@ -1,16 +1,16 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
import { Address, Transaction } from '../../interfaces/electrs.interface';
import { StateService } from '../../services/state.service';
import { OpenGraphService } from '../../services/opengraph.service';
import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { Address, Transaction } from '@interfaces/electrs.interface';
import { StateService } from '@app/services/state.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { AudioService } from '@app/services/audio.service';
import { ApiService } from '@app/services/api.service';
import { of, merge, Subscription, Observable } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface';
import { SeoService } from '@app/services/seo.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { AddressInformation } from '@interfaces/node-api.interface';
@Component({
selector: 'app-address-preview',

View File

@@ -117,7 +117,7 @@
</h2>
</div>
<app-transactions-list [transactions]="transactions" [showConfirmations]="true" [address]="address.address" (loadMore)="loadMore()"></app-transactions-list>
<app-transactions-list [transactions]="transactions" [showConfirmations]="true" [addresses]="[address.address]" (loadMore)="loadMore()"></app-transactions-list>
<div class="text-center">
<ng-template [ngIf]="isLoadingTransactions">

View File

@@ -1,17 +1,17 @@
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
import { Address, ChainStats, Transaction, Utxo, Vin } from '../../interfaces/electrs.interface';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { Address, ChainStats, Transaction, Utxo, Vin } from '@interfaces/electrs.interface';
import { WebsocketService } from '@app/services/websocket.service';
import { StateService } from '@app/services/state.service';
import { AudioService } from '@app/services/audio.service';
import { ApiService } from '@app/services/api.service';
import { of, merge, Subscription, Observable, forkJoin } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface';
import { AddressTypeInfo } from '../../shared/address-utils';
import { SeoService } from '@app/services/seo.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { AddressInformation } from '@interfaces/node-api.interface';
import { AddressTypeInfo } from '@app/shared/address-utils';
class AddressStats implements ChainStats {
address: string;

View File

@@ -0,0 +1,10 @@
<div class="addresses-treemap-container">
<div *ngIf="addresses" style="height: 300px">
<div *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
</div>
</div>
<div *ngIf="!stateService.isBrowser || isLoading" class="text-center loading-spinner">
<div class="spinner-border text-light"></div>
</div>
</div>

View File

@@ -0,0 +1,17 @@
.node-channels-container {
position: relative;
}
.loading-spinner {
position: absolute;
top: 0;
left: 0;
right: 0;
width: 100%;
z-index: 100;
}
.spinner-border {
position: relative;
top: 225px;
}

View File

@@ -0,0 +1,150 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges } from '@angular/core';
import { Router } from '@angular/router';
import { EChartsOption, TreemapSeriesOption } from '@app/graphs/echarts';
import { lerpColor } from '@app/shared/graphs.utils';
import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
import { LightningApiService } from '@app/lightning/lightning-api.service';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '@app/services/state.service';
import { Address } from '@interfaces/electrs.interface';
import { formatNumber } from '@angular/common';
@Component({
selector: 'app-addresses-treemap',
templateUrl: './addresses-treemap.component.html',
styleUrls: ['./addresses-treemap.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddressesTreemap implements OnChanges {
@Input() addresses: Address[];
@Input() isLoading: boolean = false;
chartInstance: any;
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
};
constructor(
@Inject(LOCALE_ID) public locale: string,
private lightningApiService: LightningApiService,
private amountShortenerPipe: AmountShortenerPipe,
private zone: NgZone,
private router: Router,
public stateService: StateService,
) {}
ngOnChanges(): void {
this.prepareChartOptions();
}
prepareChartOptions(): void {
const data = this.addresses.map(address => ({
address: address.address,
value: address.chain_stats.funded_txo_sum - address.chain_stats.spent_txo_sum,
stats: address.chain_stats,
}));
// only consider visible items for the color gradient
const totalValue = data.reduce((acc, address) => acc + address.value, 0);
const maxTxs = data.filter(address => address.value > (totalValue / 2000)).reduce((max, address) => Math.max(max, address.stats.tx_count), 0);
const dataItems = data.map(address => ({
...address,
itemStyle: {
color: lerpColor('#1E88E5', '#D81B60', address.stats.tx_count / maxTxs),
}
}));
this.chartOptions = {
tooltip: {
trigger: 'item',
textStyle: {
align: 'left',
}
},
series: <TreemapSeriesOption[]>[
{
height: 300,
left: 0,
right: 0,
bottom: 0,
top: 0,
roam: false,
type: 'treemap',
data: dataItems,
nodeClick: 'link',
progressive: 100,
tooltip: {
show: true,
backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: {
color: '#b1b1b1',
},
borderColor: '#000',
formatter: (value): string => {
if (!value.data.address) {
return '';
}
return `
<table style="table-layout: fixed;">
<tbody>
<tr>
<td colspan="2"><b style="color: white; margin-left: 2px">${value.data.address}</b></td>
</tr>
<tr>
<td>Received</td>
<td style="text-align: right">${this.formatValue(value.data.stats.funded_txo_sum)}</td>
</tr>
<tr>
<td>Sent</td>
<td style="text-align: right">${this.formatValue(value.data.stats.spent_txo_sum)}</td>
</tr>
<tr>
<td>Balance</td>
<td style="text-align: right">${this.formatValue(value.data.stats.funded_txo_sum - value.data.stats.spent_txo_sum)}</td>
</tr>
<tr>
<td>Transaction count</td>
<td style="text-align: right">${value.data.stats.tx_count}</td>
</tr>
</tbody>
</table>
`;
}
},
itemStyle: {
borderColor: 'black',
borderWidth: 1,
},
breadcrumb: {
show: false,
}
}
]
};
}
formatValue(sats: number): string {
if (sats > 100000000) {
return formatNumber(sats / 100000000, this.locale, '1.2-2') + ' BTC';
} else {
return this.amountShortenerPipe.transform(sats, 2) + ' sats';
}
}
onChartInit(ec: any): void {
this.chartInstance = ec;
this.chartInstance.on('click', (e) => {
//@ts-ignore
if (!e.data.address) {
return;
}
this.zone.run(() => {
//@ts-ignore
const url = new RelativeUrlPipe(this.stateService).transform(`/address/${e.data.address}`);
this.router.navigate([url]);
});
});
}
}

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { StorageService } from '../../services/storage.service';
import { StateService } from '../../services/state.service';
import { StorageService } from '@app/services/storage.service';
import { StateService } from '@app/services/state.service';
@Component({
selector: 'app-amount-selector',

View File

@@ -1,7 +1,7 @@
import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { StateService } from '../../services/state.service';
import { StateService } from '@app/services/state.service';
import { Observable, Subscription } from 'rxjs';
import { Price } from '../../services/price.service';
import { Price } from '@app/services/price.service';
@Component({
selector: 'app-amount',

View File

@@ -1,11 +1,11 @@
import { Location } from '@angular/common';
import { Component, HostListener, OnInit, Inject, LOCALE_ID, HostBinding } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { StateService } from '../../services/state.service';
import { OpenGraphService } from '../../services/opengraph.service';
import { StateService } from '@app/services/state.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap';
import { ThemeService } from '../../services/theme.service';
import { SeoService } from '../../services/seo.service';
import { ThemeService } from '@app/services/theme.service';
import { SeoService } from '@app/services/seo.service';
@Component({
selector: 'app-root',

View File

@@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { moveDec } from '../../bitcoin.utils';
import { AssetsService } from '../../services/assets.service';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { environment } from '../../../environments/environment';
import { moveDec } from '@app/bitcoin.utils';
import { AssetsService } from '@app/services/assets.service';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { environment } from '@environments/environment';
@Component({
selector: 'app-asset-circulation',

View File

@@ -1,17 +1,17 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, filter, catchError, take } from 'rxjs/operators';
import { Asset, Transaction } from '../../interfaces/electrs.interface';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '../../services/state.service';
import { AudioService } from '../../services/audio.service';
import { ApiService } from '../../services/api.service';
import { Asset, Transaction } from '@interfaces/electrs.interface';
import { WebsocketService } from '@app/services/websocket.service';
import { StateService } from '@app/services/state.service';
import { AudioService } from '@app/services/audio.service';
import { ApiService } from '@app/services/api.service';
import { of, merge, Subscription, combineLatest } from 'rxjs';
import { SeoService } from '../../services/seo.service';
import { environment } from '../../../environments/environment';
import { AssetsService } from '../../services/assets.service';
import { moveDec } from '../../bitcoin.utils';
import { SeoService } from '@app/services/seo.service';
import { environment } from '@environments/environment';
import { AssetsService } from '@app/services/assets.service';
import { moveDec } from '@app/bitcoin.utils';
@Component({
selector: 'app-asset',

View File

@@ -2,8 +2,8 @@ import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { combineLatest, Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { ApiService } from '../../../services/api.service';
import { AssetsService } from '../../../services/assets.service';
import { ApiService } from '@app/services/api.service';
import { AssetsService } from '@app/services/assets.service';
@Component({
selector: 'app-asset-group',

View File

@@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiService } from '../../../services/api.service';
import { StateService } from '../../../services/state.service';
import { ApiService } from '@app/services/api.service';
import { StateService } from '@app/services/state.service';
@Component({
selector: 'app-assets-featured',

View File

@@ -4,12 +4,12 @@ import { Router } from '@angular/router';
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
import { merge, Observable, of, Subject } from 'rxjs';
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
import { AssetExtended } from '../../../interfaces/electrs.interface';
import { AssetsService } from '../../../services/assets.service';
import { SeoService } from '../../../services/seo.service';
import { StateService } from '../../../services/state.service';
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe';
import { environment } from '../../../../environments/environment';
import { AssetExtended } from '@interfaces/electrs.interface';
import { AssetsService } from '@app/services/assets.service';
import { SeoService } from '@app/services/seo.service';
import { StateService } from '@app/services/state.service';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { environment } from '@environments/environment';
@Component({
selector: 'app-assets-nav',

View File

@@ -1,13 +1,13 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { AssetsService } from '../../services/assets.service';
import { environment } from '../../../environments/environment';
import { AssetsService } from '@app/services/assets.service';
import { environment } from '@environments/environment';
import { UntypedFormGroup } from '@angular/forms';
import { filter, map, switchMap, take } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
import { combineLatest, Observable } from 'rxjs';
import { AssetExtended } from '../../interfaces/electrs.interface';
import { SeoService } from '../../services/seo.service';
import { StateService } from '../../services/state.service';
import { AssetExtended } from '@interfaces/electrs.interface';
import { SeoService } from '@app/services/seo.service';
import { StateService } from '@app/services/state.service';
@Component({
selector: 'app-assets',

View File

@@ -4,10 +4,10 @@
<div class="item">
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
<div class="card-text">
{{ ((addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
{{ ((total) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
</div>
<div class="symbol">
<app-fiat [value]="(addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum)"></app-fiat>
<app-fiat [value]="(total)"></app-fiat>
</div>
</div>
<div class="item">

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { StateService } from '@app/services/state.service';
import { Address, AddressTxSummary } from '@interfaces/electrs.interface';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { Observable, catchError, of } from 'rxjs';
@Component({
@@ -19,6 +19,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
isLoading: boolean = true;
error: any;
total: number = 0;
delta7d: number = 0;
delta30d: number = 0;
@@ -34,7 +35,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true;
if (!this.address || !this.addressInfo) {
if (!this.addressSummary$ && (!this.address || !this.addressInfo)) {
return;
}
(this.addressSummary$ || (this.isPubkey
@@ -57,6 +58,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
calculateStats(summary: AddressTxSummary[]): void {
let weekTotal = 0;
let monthTotal = 0;
this.total = this.addressInfo ? this.addressInfo.chain_stats.funded_txo_sum - this.addressInfo.chain_stats.spent_txo_sum : summary.reduce((acc, tx) => acc + tx.value, 0);
const weekAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (7 * 24 * 60 * 60 * 1000)).getTime()) / 1000;
const monthAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (30 * 24 * 60 * 60 * 1000)).getTime()) / 1000;

View File

@@ -4,7 +4,7 @@ import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { Subscription, of, timer } from 'rxjs';
import { filter, repeat, retry, switchMap, take, tap } from 'rxjs/operators';
import { ServicesApiServices } from '../../services/services-api.service';
import { ServicesApiServices } from '@app/services/services-api.service';
@Component({
selector: 'app-bitcoin-invoice',

View File

@@ -1,17 +1,17 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { echarts, EChartsOption } from '@app/graphs/echarts';
import { Observable, combineLatest, of } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { ApiService } from '@app/services/api.service';
import { SeoService } from '@app/services/seo.service';
import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
import { StorageService } from '../../services/storage.service';
import { MiningService } from '../../services/mining.service';
import { selectPowerOfTen } from '../../bitcoin.utils';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '../../services/state.service';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '@app/shared/graphs.utils';
import { StorageService } from '@app/services/storage.service';
import { MiningService } from '@app/services/mining.service';
import { selectPowerOfTen } from '@app/bitcoin.utils';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '@app/services/state.service';
import { ActivatedRoute, Router } from '@angular/router';
@Component({

View File

@@ -1,18 +1,18 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { echarts, EChartsOption } from '@app/graphs/echarts';
import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { ApiService } from '@app/services/api.service';
import { SeoService } from '@app/services/seo.service';
import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis } from '../../shared/graphs.utils';
import { StorageService } from '../../services/storage.service';
import { MiningService } from '../../services/mining.service';
import { download, formatterXAxis } from '@app/shared/graphs.utils';
import { StorageService } from '@app/services/storage.service';
import { MiningService } from '@app/services/mining.service';
import { ActivatedRoute } from '@angular/router';
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
import { StateService } from '../../services/state.service';
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { StateService } from '@app/services/state.service';
@Component({
selector: 'app-block-fees-graph',

View File

@@ -1,19 +1,19 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
import { EChartsOption } from '../../graphs/echarts';
import { EChartsOption } from '@app/graphs/echarts';
import { Observable } from 'rxjs';
import { catchError, map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { ApiService } from '@app/services/api.service';
import { SeoService } from '@app/services/seo.service';
import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis } from '../../shared/graphs.utils';
import { download, formatterXAxis } from '@app/shared/graphs.utils';
import { ActivatedRoute, Router } from '@angular/router';
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
import { StateService } from '../../services/state.service';
import { MiningService } from '../../services/mining.service';
import { StorageService } from '../../services/storage.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { StateService } from '@app/services/state.service';
import { MiningService } from '@app/services/mining.service';
import { StorageService } from '@app/services/storage.service';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
@Component({
selector: 'app-block-fees-subsidy-graph',

View File

@@ -1,6 +1,6 @@
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
import { ActiveFilter, FilterGroups, FilterMode, GradientMode, TransactionFilters } from '../../shared/filters.utils';
import { StateService } from '../../services/state.service';
import { ActiveFilter, FilterGroups, FilterMode, GradientMode, TransactionFilters } from '@app/shared/filters.utils';
import { StateService } from '@app/services/state.service';
import { Subscription } from 'rxjs';
@@ -115,4 +115,4 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
ngOnDestroy(): void {
this.filterSubscription.unsubscribe();
}
}
}

View File

@@ -1,16 +1,16 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
import { EChartsOption } from '../../graphs/echarts';
import { EChartsOption } from '@app/graphs/echarts';
import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { ApiService } from '@app/services/api.service';
import { SeoService } from '@app/services/seo.service';
import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
import { StorageService } from '../../services/storage.service';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '@app/shared/graphs.utils';
import { StorageService } from '@app/services/storage.service';
import { ActivatedRoute, Router } from '@angular/router';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '../../services/state.service';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '@app/services/state.service';
@Component({
selector: 'app-block-health-graph',

View File

@@ -1,17 +1,17 @@
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { FastVertexArray } from './fast-vertex-array';
import BlockScene from './block-scene';
import TxSprite from './tx-sprite';
import TxView from './tx-view';
import { Color, Position } from './sprite-types';
import { Price } from '../../services/price.service';
import { StateService } from '../../services/state.service';
import { ThemeService } from '../../services/theme.service';
import { TransactionStripped } from '@interfaces/node-api.interface';
import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array';
import BlockScene from '@components/block-overview-graph/block-scene';
import TxSprite from '@components/block-overview-graph/tx-sprite';
import TxView from '@components/block-overview-graph/tx-view';
import { Color, Position } from '@components/block-overview-graph/sprite-types';
import { Price } from '@app/services/price.service';
import { StateService } from '@app/services/state.service';
import { ThemeService } from '@app/services/theme.service';
import { Subscription } from 'rxjs';
import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction, contrastColorFunction, contrastAuditColors, contrastColors } from './utils';
import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils';
import { detectWebGL } from '../../shared/graphs.utils';
import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction, contrastColorFunction, contrastAuditColors, contrastColors } from '@components/block-overview-graph/utils';
import { ActiveFilter, FilterMode, toFlags } from '@app/shared/filters.utils';
import { detectWebGL } from '@app/shared/graphs.utils';
const unmatchedOpacity = 0.2;
const unmatchedAuditColors = {

View File

@@ -1,9 +1,9 @@
import { FastVertexArray } from './fast-vertex-array';
import TxView from './tx-view';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { Color, Position, Square, ViewUpdateParams } from './sprite-types';
import { defaultColorFunction, contrastColorFunction } from './utils';
import { ThemeService } from '../../services/theme.service';
import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array';
import TxView from '@components/block-overview-graph/tx-view';
import { TransactionStripped } from '@interfaces/node-api.interface';
import { Color, Position, Square, ViewUpdateParams } from '@components/block-overview-graph/sprite-types';
import { defaultColorFunction, contrastColorFunction } from '@components/block-overview-graph/utils';
import { ThemeService } from '@app/services/theme.service';
export default class BlockScene {
scene: { count: number, offset: { x: number, y: number}};
@@ -917,4 +917,4 @@ class BlockLayout {
function feeRateDescending(a: TxView, b: TxView) {
return b.feerate - a.feerate;
}
}

View File

@@ -8,7 +8,7 @@
or compacting into a smaller Float32Array when there's space to do so.
*/
import TxSprite from './tx-sprite';
import TxSprite from '@components/block-overview-graph/tx-sprite';
export class FastVertexArray {
length: number;

View File

@@ -1,5 +1,5 @@
import { FastVertexArray } from './fast-vertex-array';
import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from './sprite-types';
import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array';
import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from '@components/block-overview-graph/sprite-types';
const attribKeys = ['a', 'b', 't', 'v'];
const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];

View File

@@ -1,10 +1,10 @@
import TxSprite from './tx-sprite';
import { FastVertexArray } from './fast-vertex-array';
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
import { hexToColor } from './utils';
import BlockScene from './block-scene';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { TransactionFlags } from '../../shared/filters.utils';
import TxSprite from '@components/block-overview-graph/tx-sprite';
import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array';
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from '@components/block-overview-graph/sprite-types';
import { hexToColor } from '@components/block-overview-graph/utils';
import BlockScene from '@components/block-overview-graph/block-scene';
import { TransactionStripped } from '@interfaces/node-api.interface';
import { TransactionFlags } from '@app/shared/filters.utils';
const hoverTransitionTime = 300;
const defaultHoverColor = hexToColor('1bd8f4');

View File

@@ -1,6 +1,6 @@
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../app.constants';
import { Color } from './sprite-types';
import TxView from './tx-view';
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '@app/app.constants';
import { Color } from '@components/block-overview-graph/sprite-types';
import TxView from '@components/block-overview-graph/tx-view';
export function hexToColor(hex: string): Color {
return {

View File

@@ -1,9 +1,9 @@
import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Position } from '../../components/block-overview-graph/sprite-types.js';
import { Price } from '../../services/price.service';
import { TransactionStripped } from '../../interfaces/node-api.interface.js';
import { Filter, FilterMode, TransactionFlags, toFilters } from '../../shared/filters.utils';
import { Block } from '../../interfaces/electrs.interface.js';
import { Position } from '@components/block-overview-graph/sprite-types.js';
import { Price } from '@app/services/price.service';
import { TransactionStripped } from '@interfaces/node-api.interface.js';
import { Filter, FilterMode, TransactionFlags, toFilters } from '@app/shared/filters.utils';
import { Block } from '@interfaces/electrs.interface.js';
@Component({
selector: 'app-block-overview-tooltip',

View File

@@ -1,18 +1,18 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { echarts, EChartsOption } from '@app/graphs/echarts';
import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { ApiService } from '@app/services/api.service';
import { SeoService } from '@app/services/seo.service';
import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis } from '../../shared/graphs.utils';
import { MiningService } from '../../services/mining.service';
import { StorageService } from '../../services/storage.service';
import { download, formatterXAxis } from '@app/shared/graphs.utils';
import { MiningService } from '@app/services/mining.service';
import { StorageService } from '@app/services/storage.service';
import { ActivatedRoute } from '@angular/router';
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
import { StateService } from '../../services/state.service';
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { StateService } from '@app/services/state.service';
@Component({
selector: 'app-block-rewards-graph',

View File

@@ -1,16 +1,16 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
import { EChartsOption} from '../../graphs/echarts';
import { EChartsOption} from '@app/graphs/echarts';
import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { SeoService } from '../../services/seo.service';
import { ApiService } from '@app/services/api.service';
import { SeoService } from '@app/services/seo.service';
import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { StorageService } from '../../services/storage.service';
import { MiningService } from '../../services/mining.service';
import { StorageService } from '@app/services/storage.service';
import { MiningService } from '@app/services/mining.service';
import { ActivatedRoute } from '@angular/router';
import { download, formatterXAxis } from '../../shared/graphs.utils';
import { StateService } from '../../services/state.service';
import { download, formatterXAxis } from '@app/shared/graphs.utils';
import { StateService } from '@app/services/state.service';
@Component({
selector: 'app-block-sizes-weights-graph',

View File

@@ -1,15 +1,15 @@
import { Component, OnInit, OnDestroy, ViewChild, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, tap, catchError, shareReplay, filter } from 'rxjs/operators';
import { of, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '@app/services/state.service';
import { SeoService } from '@app/services/seo.service';
import { BlockExtended, TransactionStripped } from '@interfaces/node-api.interface';
import { ApiService } from '@app/services/api.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { BlockOverviewGraphComponent } from '@components/block-overview-graph/block-overview-graph.component';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
function bestFitResolution(min, max, n): number {
const target = (min + max) / 2;

View File

@@ -1,16 +1,16 @@
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, tap, throttleTime, catchError, shareReplay, startWith, pairwise, filter } from 'rxjs/operators';
import { of, Subscription, asyncScheduler, forkJoin } from 'rxjs';
import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { OpenGraphService } from '../../services/opengraph.service';
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
import { ServicesApiServices } from '../../services/services-api.service';
import { StateService } from '@app/services/state.service';
import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { BlockExtended, TransactionStripped } from '@interfaces/node-api.interface';
import { ApiService } from '@app/services/api.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { BlockOverviewGraphComponent } from '@components/block-overview-graph/block-overview-graph.component';
import { ServicesApiServices } from '@app/services/services-api.service';
@Component({
selector: 'app-block-preview',

View File

@@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Transaction, Vout } from '../../interfaces/electrs.interface';
import { StateService } from '@app/services/state.service';
import { Transaction, Vout } from '@interfaces/electrs.interface';
import { Observable, Subscription, catchError, combineLatest, map, of, startWith, switchMap, tap } from 'rxjs';
import { ActivatedRoute, Router } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { PreloadService } from '../../services/preload.service';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { PreloadService } from '@app/services/preload.service';
@Component({
selector: 'app-block-transactions',

View File

@@ -1,23 +1,23 @@
import { Component, OnInit, OnDestroy, ViewChildren, QueryList, ChangeDetectorRef } from '@angular/core';
import { Location } from '@angular/common';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, filter } from 'rxjs/operators';
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs';
import { StateService } from '../../services/state.service';
import { SeoService } from '../../services/seo.service';
import { WebsocketService } from '../../services/websocket.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { Acceleration, BlockAudit, BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component';
import { detectWebGL } from '../../shared/graphs.utils';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { PriceService, Price } from '../../services/price.service';
import { CacheService } from '../../services/cache.service';
import { ServicesApiServices } from '../../services/services-api.service';
import { PreloadService } from '../../services/preload.service';
import { identifyPrioritizedTransactions } from '../../shared/transaction.utils';
import { StateService } from '@app/services/state.service';
import { SeoService } from '@app/services/seo.service';
import { WebsocketService } from '@app/services/websocket.service';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { Acceleration, BlockAudit, BlockExtended, TransactionStripped } from '@interfaces/node-api.interface';
import { ApiService } from '@app/services/api.service';
import { BlockOverviewGraphComponent } from '@components/block-overview-graph/block-overview-graph.component';
import { detectWebGL } from '@app/shared/graphs.utils';
import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { PriceService, Price } from '@app/services/price.service';
import { CacheService } from '@app/services/cache.service';
import { ServicesApiServices } from '@app/services/services-api.service';
import { PreloadService } from '@app/services/preload.service';
import { identifyPrioritizedTransactions } from '@app/shared/transaction.utils';
@Component({
selector: 'app-block',
@@ -822,4 +822,4 @@ export class BlockComponent implements OnInit, OnDestroy {
this.fees = blockReward;
}
}
}
}

View File

@@ -1,9 +1,9 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { BlockComponent } from './block.component';
import { BlockTransactionsComponent } from './block-transactions.component';
import { SharedModule } from '../../shared/shared.module';
import { BlockComponent } from '@components/block/block.component';
import { BlockTransactionsComponent } from '@components/block/block-transactions.component';
import { SharedModule } from '@app/shared/shared.module';
const routes: Routes = [
{

View File

@@ -1,10 +1,10 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnChanges, SimpleChanges } from '@angular/core';
import { Observable, Subscription, delay, filter, tap } from 'rxjs';
import { StateService } from '../../services/state.service';
import { specialBlocks } from '../../app.constants';
import { BlockExtended } from '../../interfaces/node-api.interface';
import { StateService } from '@app/services/state.service';
import { specialBlocks } from '@app/app.constants';
import { BlockExtended } from '@interfaces/node-api.interface';
import { Location } from '@angular/common';
import { CacheService } from '../../services/cache.service';
import { CacheService } from '@app/services/cache.service';
interface BlockchainBlock extends BlockExtended {
placeholder?: boolean;

View File

@@ -1,7 +1,7 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, Output, EventEmitter, ChangeDetectorRef, OnChanges, SimpleChanges } from '@angular/core';
import { firstValueFrom, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
import { StorageService } from '../../services/storage.service';
import { StateService } from '@app/services/state.service';
import { StorageService } from '@app/services/storage.service';
@Component({
selector: 'app-blockchain',

View File

@@ -2,13 +2,14 @@ import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, I
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject, combineLatest, Observable, timer, of, Subscription } from 'rxjs';
import { debounceTime, delayWhen, filter, map, retryWhen, scan, skip, switchMap, tap, throttleTime } from 'rxjs/operators';
import { BlockExtended } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { SeoService } from '../../services/seo.service';
import { OpenGraphService } from '../../services/opengraph.service';
import { seoDescriptionNetwork } from '../../shared/common.utils';
import { BlockExtended } from '@interfaces/node-api.interface';
import { ApiService } from '@app/services/api.service';
import { StateService } from '@app/services/state.service';
import { WebsocketService } from '@app/services/websocket.service';
import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
@Component({
selector: 'app-blocks-list',
@@ -49,6 +50,7 @@ export class BlocksList implements OnInit {
private ogService: OpenGraphService,
private route: ActivatedRoute,
private router: Router,
private relativeUrlPipe: RelativeUrlPipe,
@Inject(LOCALE_ID) private locale: string,
) {
this.isMempoolModule = this.stateService.env.BASE_MODULE === 'mempool';
@@ -182,7 +184,7 @@ export class BlocksList implements OnInit {
}
pageChange(page: number): void {
this.router.navigate(['blocks', page]);
this.router.navigate([this.relativeUrlPipe.transform('/blocks/'), page]);
}
trackByBlock(index: number, block: BlockExtended): number {

View File

@@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { combineLatest, Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { StateService } from '@app/services/state.service';
import { WebsocketService } from '@app/services/websocket.service';
@Component({
selector: 'app-calculator',

View File

@@ -1,15 +1,17 @@
<ng-template [ngIf]="button" [ngIfElse]="btnLink">
<button #btn [attr.data-clipboard-text]="text" [class]="class" type="button" [disabled]="text === ''">
<span #buttonWrapper [attr.data-tlite]="copiedMessage" style="position: relative;top: -2px;left: 1px;">
<button [class]="class" type="button" [disabled]="text === ''" style="box-shadow: none;" (click)="copyText()">
<span style="position: relative;top: -2px;left: 1px;">
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images>
<span *ngIf="showMessage" class="copied-message" style="top: 29px; left: -23.5px;">{{ copiedMessage }}</span>
</span>
</button>
</ng-template>
<ng-template #btnLink>
<span #buttonWrapper [attr.data-tlite]="copiedMessage" style="position: relative;">
<button #btn class="btn btn-sm btn-link pt-0 {{ leftPadding ? 'padding' : '' }}" [attr.data-clipboard-text]="text">
<span style="position: relative;">
<button class="btn btn-sm btn-link pt-0 {{ leftPadding ? 'padding' : '' }}" style="box-shadow: none;" (click)="copyText()">
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images>
</button>
<span *ngIf="showMessage" class="copied-message" style="top: 29px; left: -23.5px;">{{ copiedMessage }}</span>
</span>
</ng-template>

View File

@@ -7,7 +7,19 @@
padding-left: 0.4rem;
}
img {
position: relative;
left: -3px;
}
.copied-message {
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
color: var(--fg);
font-family: sans-serif;
font-size: .8rem;
font-weight: 400;
text-decoration: none;
text-align: left;
padding: .6em .75rem;
border-radius: 4px;
position: absolute;
white-space: nowrap;
box-shadow: 0 .5rem 1rem -.5rem #000;
z-index: 1000;
opacity: .9;
}

View File

@@ -1,6 +1,4 @@
import { Component, ViewChild, ElementRef, AfterViewInit, Input, ChangeDetectionStrategy } from '@angular/core';
import * as ClipboardJS from 'clipboard';
import * as tlite from 'tlite';
import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-clipboard',
@@ -8,15 +6,14 @@ import * as tlite from 'tlite';
styleUrls: ['./clipboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClipboardComponent implements AfterViewInit {
@ViewChild('btn') btn: ElementRef;
@ViewChild('buttonWrapper') buttonWrapper: ElementRef;
export class ClipboardComponent {
@Input() button = false;
@Input() class = 'btn btn-secondary ml-1';
@Input() size: 'small' | 'normal' | 'large' = 'normal';
@Input() text: string;
@Input() leftPadding = true;
copiedMessage: string = $localize`:@@clipboard.copied-message:Copied!`;
showMessage = false;
widths = {
small: '10',
@@ -24,22 +21,40 @@ export class ClipboardComponent implements AfterViewInit {
large: '18',
};
clipboard: any;
constructor(
private cd: ChangeDetectorRef,
) { }
constructor() { }
ngAfterViewInit() {
this.clipboard = new ClipboardJS(this.btn.nativeElement);
this.clipboard.on('success', () => {
tlite.show(this.buttonWrapper.nativeElement);
setTimeout(() => {
tlite.hide(this.buttonWrapper.nativeElement);
}, 1000);
});
async copyText() {
if (this.text && !this.showMessage) {
try {
await this.copyToClipboard(this.text);
this.showMessage = true;
this.cd.markForCheck();
setTimeout(() => {
this.showMessage = false;
this.cd.markForCheck();
}, 1000);
} catch (error) {
console.error('Clipboard copy failed:', error);
}
}
}
onDestroy() {
this.clipboard.destroy();
async copyToClipboard(text: string) {
if (navigator.clipboard) {
await navigator.clipboard.writeText(text);
} else {
// Use the 'out of viewport hidden text area' trick on non-secure contexts
const textarea = document.createElement('textarea');
textarea.value = this.text;
textarea.style.opacity = '0';
textarea.setAttribute('readonly', 'true'); // Don't trigger keyboard on mobile
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
}
}
}

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { Subscription, tap, timer } from 'rxjs';
import { StateService } from '../../services/state.service';
import { StateService } from '@app/services/state.service';
@Component({
selector: 'app-clock-face',

View File

@@ -1,11 +1,11 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Input, OnInit } from '@angular/core';
import { Observable, Subscription, of, switchMap, tap } from 'rxjs';
import { StateService } from '../../services/state.service';
import { BlockExtended } from '../../interfaces/node-api.interface';
import { WebsocketService } from '../../services/websocket.service';
import { MempoolInfo, Recommendedfees } from '../../interfaces/websocket.interface';
import { StateService } from '@app/services/state.service';
import { BlockExtended } from '@interfaces/node-api.interface';
import { WebsocketService } from '@app/services/websocket.service';
import { MempoolInfo, Recommendedfees } from '@interfaces/websocket.interface';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
@Component({
selector: 'app-clock',

View File

@@ -1,6 +1,6 @@
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, OnChanges, ChangeDetectorRef } from '@angular/core';
import { firstValueFrom, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
import { StateService } from '@app/services/state.service';
@Component({
selector: 'app-clockchain',

View File

@@ -257,6 +257,38 @@
</div>
</div>
}
@case ('walletBalance') {
<div class="col card-wrapper" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="main-title" i18n="dashboard.treasury">Treasury</div>
<app-balance-widget [addressSummary$]="walletSummary$"></app-balance-widget>
</div>
}
@case ('wallet') {
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card">
<div class="card-body">
<a class="title-link mb-0" style="margin-top: -2px" href="" [routerLink]="['/wallet/' + widget.props.wallet | relativeUrl]">
<h5 class="card-title d-inline" i18n="dashboard.balance-history">Balance History</h5>
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: var(--title-fg)"></fa-icon>
</a>
<app-address-graph [addressSummary$]="walletSummary$" [period]="widget.props.period || 'all'" [widget]="true" [height]="graphHeight"></app-address-graph>
</div>
</div>
</div>
}
@case ('walletTransactions') {
<div class="col" style="max-height: 410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card">
<div class="card-body">
<span class="title-link">
<h5 class="card-title d-inline" i18n="dashboard.treasury-transactions">Treasury Transactions</h5>
</span>
<app-address-transactions-widget [addressSummary$]="walletSummary$"></app-address-transactions-widget>
</div>
</div>
</div>
}
@case ('twitter') {
<div class="col" style="min-height:410px" [style.order]="isMobile && widget.mobileOrder || 8">
<div class="card graph-card">

View File

@@ -1,16 +1,16 @@
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { combineLatest, merge, Observable, of, Subject, Subscription } from 'rxjs';
import { catchError, filter, map, scan, share, shareReplay, startWith, switchMap, tap } from 'rxjs/operators';
import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '../../interfaces/node-api.interface';
import { MempoolInfo, ReplacementInfo } from '../../interfaces/websocket.interface';
import { ApiService } from '../../services/api.service';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { SeoService } from '../../services/seo.service';
import { ActiveFilter, FilterMode, GradientMode, toFlags } from '../../shared/filters.utils';
import { detectWebGL } from '../../shared/graphs.utils';
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '@interfaces/node-api.interface';
import { MempoolInfo, ReplacementInfo } from '@interfaces/websocket.interface';
import { ApiService } from '@app/services/api.service';
import { StateService } from '@app/services/state.service';
import { WebsocketService } from '@app/services/websocket.service';
import { SeoService } from '@app/services/seo.service';
import { ActiveFilter, FilterMode, GradientMode, toFlags } from '@app/shared/filters.utils';
import { detectWebGL } from '@app/shared/graphs.utils';
import { Address, AddressTxSummary } from '@interfaces/electrs.interface';
import { ElectrsApiService } from '@app/services/electrs-api.service';
interface MempoolBlocksData {
blocks: number;
@@ -62,8 +62,10 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
widgets;
addressSubscription: Subscription;
walletSubscription: Subscription;
blockTxSubscription: Subscription;
addressSummary$: Observable<AddressTxSummary[]>;
walletSummary$: Observable<AddressTxSummary[]>;
address: Address;
goggleResolution = 82;
@@ -71,7 +73,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
{ index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' },
{ index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' },
{ index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' },
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'fake_scripthash', 'op_return'], gradient: 'fee' },
];
goggleFlags = 0n;
goggleMode: FilterMode = 'and';
@@ -107,6 +109,10 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
this.websocketService.stopTrackingAddress();
this.address = null;
}
if (this.walletSubscription) {
this.walletSubscription.unsubscribe();
this.websocketService.stopTrackingWallet();
}
this.destroy$.next(1);
this.destroy$.complete();
}
@@ -260,6 +266,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
});
this.startAddressSubscription();
this.startWalletSubscription();
}
handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
@@ -358,6 +365,75 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
}
}
startWalletSubscription(): void {
if (this.stateService.env.customize && this.stateService.env.customize.dashboard.widgets.some(w => w.props?.wallet)) {
const walletName = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.wallet).props.wallet;
this.websocketService.startTrackingWallet(walletName);
this.walletSummary$ = this.apiService.getWallet$(walletName).pipe(
catchError(e => {
return of({});
}),
switchMap(wallet => this.stateService.walletTransactions$.pipe(
startWith([]),
scan((summaries, newTransactions) => {
const newSummaries: AddressTxSummary[] = [];
for (const tx of newTransactions) {
const funded: Record<string, number> = {};
const spent: Record<string, number> = {};
const fundedCount: Record<string, number> = {};
const spentCount: Record<string, number> = {};
for (const vin of tx.vin) {
const address = vin.prevout?.scriptpubkey_address;
if (address && wallet[address]) {
spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0);
spentCount[address] = (spentCount[address] ?? 0) + 1;
}
}
for (const vout of tx.vout) {
const address = vout.scriptpubkey_address;
if (address && wallet[address]) {
funded[address] = (funded[address] ?? 0) + (vout.value ?? 0);
fundedCount[address] = (fundedCount[address] ?? 0) + 1;
}
}
for (const address of Object.keys({ ...funded, ...spent })) {
// add tx to summary
const txSummary: AddressTxSummary = {
txid: tx.txid,
value: (funded[address] ?? 0) - (spent[address] ?? 0),
height: tx.status.block_height,
time: tx.status.block_time,
};
wallet[address].transactions?.push(txSummary);
newSummaries.push(txSummary);
}
}
return this.deduplicateWalletTransactions([...summaries, ...newSummaries]);
}, this.deduplicateWalletTransactions(Object.values(wallet).flatMap(address => address.transactions)))
)),
share(),
);
}
}
deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
const transactions = new Map<string, AddressTxSummary>();
for (const tx of walletTransactions) {
if (transactions.has(tx.txid)) {
transactions.get(tx.txid).value += tx.value;
} else {
transactions.set(tx.txid, tx);
}
}
return Array.from(transactions.values()).sort((a, b) => {
if (a.height === b.height) {
return b.tx_position - a.tx_position;
}
return b.height - a.height;
});
}
@HostListener('window:resize', ['$event'])
onResize(): void {
if (window.innerWidth >= 992) {

View File

@@ -1,10 +1,10 @@
import { Component, Inject, LOCALE_ID, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { ApiService } from '@app/services/api.service';
import { formatNumber } from '@angular/common';
import { selectPowerOfTen } from '../../bitcoin.utils';
import { StateService } from '../../services/state.service';
import { selectPowerOfTen } from '@app/bitcoin.utils';
import { StateService } from '@app/services/state.service';
@Component({
selector: 'app-difficulty-adjustments-table',

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { StateService } from '../../services/state.service';
import { StateService } from '@app/services/state.service';
interface EpochProgress {
base: string;

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, ElementRef, ViewChild, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { StateService } from '../..//services/state.service';
import { StateService } from '@app/services/state.service';
interface EpochProgress {
base: string;
@@ -247,4 +247,4 @@ function getNextBlockSubsidy(height: number): number {
// Subsidy is cut in half every 210,000 blocks which will occur approximately every 4 years.
subsidy >>= BigInt(halvings);
return Number(subsidy);
}
}

View File

@@ -2,15 +2,15 @@ import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/
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 { StateService } from '@app/services/state.service';
import { WebsocketService } from '@app/services/websocket.service';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { BlockExtended, TransactionStripped } from '@interfaces/node-api.interface';
import { ApiService } from '@app/services/api.service';
import { BlockOverviewGraphComponent } from '@components/block-overview-graph/block-overview-graph.component';
import { detectWebGL } from '@app/shared/graphs.utils';
import { animate, style, transition, trigger } from '@angular/animations';
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
import { BytesPipe } from '@app/shared/pipes/bytes-pipe/bytes.pipe';
function bestFitResolution(min, max, n): number {
const target = (min + max) / 2;

View File

@@ -1,12 +1,12 @@
import { Component, OnDestroy, OnInit, ChangeDetectorRef } from "@angular/core";
import { FormBuilder, FormGroup, Validators, ValidatorFn, AbstractControl, ValidationErrors } from "@angular/forms";
import { Subscription } from "rxjs";
import { ServicesApiServices } from "../../services/services-api.service";
import { getRegex } from "../../shared/regex.utils";
import { StateService } from "../../services/state.service";
import { WebsocketService } from "../../services/websocket.service";
import { AudioService } from "../../services/audio.service";
import { HttpErrorResponse } from "@angular/common/http";
import { Component, OnDestroy, OnInit, ChangeDetectorRef } from '@angular/core';
import { FormBuilder, FormGroup, Validators, ValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms';
import { Subscription } from 'rxjs';
import { ServicesApiServices } from '@app/services/services-api.service';
import { getRegex } from '@app/shared/regex.utils';
import { StateService } from '@app/services/state.service';
import { WebsocketService } from '@app/services/websocket.service';
import { AudioService } from '@app/services/audio.service';
import { HttpErrorResponse } from '@angular/common/http';
@Component({
selector: 'app-faucet',

Some files were not shown because too many files have changed in this diff Show More