Compare commits

..

39 Commits

Author SHA1 Message Date
natsoni
c6285dfa26 Merge branch 'master' into natsoni/fix-tooltip-timeline 2024-10-25 11:52:25 +02: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
nymkappa
c950e3d0ae [core routes] /api/internal -> /api/v1/internal 2024-10-25 13:54:40 +09: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
510b4adcea Merge branch 'master' into natsoni/fix-tooltip-timeline 2024-10-04 21:59:07 -06:00
natsoni
0928d64e1e Fix timeline tooltip on mobile 2024-09-27 18:59:23 +02:00
294 changed files with 3559 additions and 3402 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

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

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

@@ -3,8 +3,7 @@ import * as WebSocket from 'ws';
import {
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
MempoolDelta, MempoolDeltaTxids,
TransactionCompressed
MempoolDelta, MempoolDeltaTxids
} from '../mempool.interfaces';
import blocks from './blocks';
import memPool from './mempool';
@@ -28,6 +27,7 @@ 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[],
@@ -308,6 +308,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'];
@@ -318,7 +326,6 @@ class WebsocketHandler {
if (parsedMessage && parsedMessage['track-mempool-block'] !== undefined) {
if (Number.isInteger(parsedMessage['track-mempool-block']) && parsedMessage['track-mempool-block'] >= 0) {
client['track-mempool-blocks'] = undefined;
const index = parsedMessage['track-mempool-block'];
client['track-mempool-block'] = index;
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
@@ -328,31 +335,7 @@ class WebsocketHandler {
blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
});
} else {
client['track-mempool-block'] = undefined;
}
}
if (parsedMessage && parsedMessage['track-mempool-blocks'] !== undefined) {
if (parsedMessage['track-mempool-blocks'].length > 0) {
client['track-mempool-block'] = undefined;
const indices: number[] = [];
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
const updates: { index: number, sequence: number, blockTransactions: TransactionCompressed[] }[] = [];
for (const i of parsedMessage['track-mempool-blocks']) {
const index = parseInt(i);
if (Number.isInteger(index) && index >= 0) {
indices.push(index);
updates.push({
index: index,
sequence: this.mempoolSequence,
blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
});
}
}
client['track-mempool-blocks'] = indices;
response['projected-block-transactions'] = JSON.stringify(updates);
} else {
client['track-mempool-blocks'] = undefined;
client['track-mempool-block'] = null;
}
}
@@ -936,19 +919,6 @@ class WebsocketHandler {
delta: mBlockDeltas[index],
});
}
} else if (client['track-mempool-blocks']?.length && memPool.isInSync()) {
const indices = client['track-mempool-blocks'];
const updates: string[] = [];
for (const index of indices) {
if (mBlockDeltas[index]) {
updates.push(getCachedResponse(`projected-block-transactions-${index}`, {
index: index,
sequence: this.mempoolSequence,
delta: mBlockDeltas[index],
}));
}
}
response['projected-block-transactions'] = '[' + updates.join(',') + ']';
}
if (client['track-rbf'] === 'all' && rbfReplacements) {
@@ -1151,6 +1121,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]) {
@@ -1345,27 +1318,6 @@ class WebsocketHandler {
});
}
}
} else if (client['track-mempool-blocks']?.length && memPool.isInSync()) {
const indices = client['track-mempool-blocks'];
const updates: string[] = [];
for (const index of indices) {
if (mBlockDeltas && mBlockDeltas[index] && mBlocksWithTransactions[index]?.transactions?.length) {
if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) {
updates.push(getCachedResponse(`projected-block-transactions-full-${index}`, {
index: index,
sequence: this.mempoolSequence,
blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx),
}));
} else {
updates.push(getCachedResponse(`projected-block-transactions-delta-${index}`, {
index: index,
sequence: this.mempoolSequence,
delta: mBlockDeltas[index],
}));
}
}
}
response['projected-block-transactions'] = '[' + updates.join(',') + ']';
}
if (client['track-mempool-txids']) {
@@ -1376,6 +1328,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;
@@ -238,6 +240,10 @@ class Server {
await memPool.$updateMempool(newMempool, newAccelerations, 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();
}
@@ -335,6 +341,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

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

@@ -1,16 +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 { EightMempoolComponent } from './components/eight-mempool/eight-mempool.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
@@ -23,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: {
@@ -46,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 },
},
{
@@ -61,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 },
},
{
@@ -84,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 },
},
{
@@ -104,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: {
@@ -127,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 },
},
{
@@ -139,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: {
@@ -166,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)
},
],
},
@@ -206,10 +205,6 @@ let routes: Routes = [
path: 'view/blocks',
component: EightBlocksComponent,
},
{
path: 'view/mempool-blocks',
component: EightMempoolComponent,
},
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },
@@ -217,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 },
},
];
@@ -230,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: {
@@ -253,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 },
},
{
@@ -265,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: {
@@ -286,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)
},
],
},
@@ -301,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 { 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 { ZONE_SERVICE } from '@app/injection-tokens';
import { AppModule } from '@app/app.module';
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 { 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 { ZONE_SERVICE } from '@app/injection-tokens';
import { AppRoutingModule } from '@app/app-routing.module';
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

@@ -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, insecureRandomUUID } 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;
@@ -143,6 +137,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
private authService: AuthServiceMempool,
private enterpriseService: EnterpriseService,
) {
this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1;
this.accelerationUUID = insecureRandomUUID();
// Check if Apple Pay available

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,11 +1,11 @@
<div
#tooltip
*ngIf="accelerationInfo && tooltipPosition !== null"
class="acceleration-tooltip"
[style.left]="tooltipPosition.x + 'px'"
[style.top]="tooltipPosition.y + 'px'"
[style.visibility]="accelerationInfo ? 'visible' : 'hidden'"
[style.left]="tooltipPosition?.x + 'px'"
[style.top]="tooltipPosition?.y + 'px'"
>
<table>
<table *ngIf="accelerationInfo">
<tbody>
<tr>
<td class="label" i18n="transaction.status|Transaction Status">Status</td>
@@ -52,8 +52,7 @@
class="pool-logo"
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
onError="this.src = '/resources/mining-pools/default.svg'"
[alt]="'Logo of ' + pool.name + ' mining pool'">
onError="this.src = '/resources/mining-pools/default.svg'">
<br *ngIf="i % 6 === 5">
</ng-container>
</td>

View File

@@ -1,4 +1,4 @@
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
import { Component, ElementRef, ViewChild, Input, OnChanges, HostListener } from '@angular/core';
@Component({
selector: 'app-acceleration-timeline-tooltip',
@@ -10,6 +10,7 @@ export class AccelerationTimelineTooltipComponent implements OnChanges {
@Input() cursorPosition: { x: number, y: number };
tooltipPosition: any = null;
yScroll = window.scrollY;
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
@@ -21,6 +22,9 @@ export class AccelerationTimelineTooltipComponent implements OnChanges {
let y = changes.cursorPosition.currentValue.y + 20;
if (this.tooltipElement) {
const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect();
if (this.accelerationInfo?.status !== 'seen') {
elementBounds.width = 370; // ugly hack to handle varying width due to pools logo loading
}
if ((x + elementBounds.width) > (window.innerWidth - 10)) {
x = Math.max(0, window.innerWidth - elementBounds.width - 10);
}
@@ -35,4 +39,12 @@ export class AccelerationTimelineTooltipComponent implements OnChanges {
hasPoolsData(): boolean {
return Object.keys(this.accelerationInfo.poolsData).length > 0;
}
@HostListener('window:scroll', ['$event'])
onWindowScroll(): void {
if (this.tooltipPosition) {
this.tooltipPosition.y = this.tooltipPosition.y - (window.scrollY - this.yScroll);
}
this.yScroll = window.scrollY;
}
}

View File

@@ -56,8 +56,8 @@
<div class="nodes">
<div class="node" [id]="'first-seen'">
<div class="seen-to-acc right"></div>
<div class="shape-border hovering" (pointerover)="onHover($event, 'seen');" (pointerout)="onBlur($event);">
<div class="shape"></div>
<div class="shape-border hovering">
<div id="step" class="shape" (pointerover)="onHover($event, 'seen');"></div>
</div>
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
<div class="time">
@@ -78,8 +78,8 @@
} @else {
<div class="seen-to-acc right"></div>
}
<div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);">
<div class="shape"></div>
<div class="shape-border hovering">
<div id="step" class="shape" (pointerover)="onHover($event, 'accelerated');"></div>
@if (!tx.status.confirmed) {
<div class="connector down loading"></div>
}
@@ -111,11 +111,8 @@
} @else {
<div class="seen-to-acc left"></div>
}
<div class="shape-border"
[ngClass]="{'waiting': !tx.status.confirmed, 'hovering': tx.status.confirmed}"
(pointerover)="onHover($event, tx.status.confirmed ? 'mined' : null)"
(pointerout)="onBlur($event);">
<div class="shape"></div>
<div class="shape-border" [ngClass]="{'waiting': !tx.status.confirmed, 'hovering': tx.status.confirmed}">
<div id="step" class="shape" (pointerover)="onHover($event, tx.status.confirmed ? 'mined' : null)" ></div>
</div>
@if (tx.status.confirmed) {
<div class="status"><span class="badge badge-success" i18n="transaction.rbf.mined">Mined</span></div>

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',
@@ -52,6 +52,7 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
}
onHover(event, status: string): void {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
if (status === 'seen') {
this.hoverInfo = {
status,
@@ -80,12 +81,19 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
}
}
onBlur(event): void {
this.hoverInfo = null;
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
if (event.target.id === 'step') {
this.tooltipPosition = { x: event.clientX, y: event.clientY };
} else {
this.hoverInfo = null;
}
}
@HostListener('document:click', ['$event'])
clickAway(event) {
if (event.target.id !== 'step') {
this.hoverInfo = null;
}
}
}

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

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

@@ -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,16 @@
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';
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
const periodSeconds = {
'1d': (60 * 60 * 24),
@@ -83,7 +83,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
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 +144,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 +173,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);

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 '@app/../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 = {
@@ -553,7 +553,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
x: cssX,
y: cssY
};
const selected = this.scene.getTxAt({ x, y: this.displayHeight - y });
const selected = this.scene.getTxAt({ x, y });
const currentPreview = this.selectedTx || this.hoverTx;
if (selected !== currentPreview) {
@@ -627,7 +627,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (this.scene) {
const x = cssX * window.devicePixelRatio;
const y = cssY * window.devicePixelRatio;
const selected = this.scene.getTxAt({ x, y: this.displayHeight - y });
const selected = this.scene.getTxAt({ x, y });
if (selected && selected.txid) {
this.txClickEvent.emit({ tx: selected, keyModifier });
}
@@ -681,9 +681,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
// WebGL shader attributes
const attribs = {
bounds: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
offset: { type: 'FLOAT', count: 2, pointer: null, offset: 0 },
posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
posY: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
posR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colG: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
@@ -706,9 +707,10 @@ varying lowp vec4 vColor;
// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
// shader interpolates between start and end values at the given rate, from the given time
attribute vec4 bounds;
attribute vec2 offset;
attribute vec4 posX;
attribute vec4 posY;
attribute vec4 posR;
attribute vec4 colR;
attribute vec4 colG;
attribute vec4 colB;
@@ -733,7 +735,10 @@ float interpolateAttribute(vec4 attr) {
void main() {
vec4 screenTransform = vec4(2.0 / screenSize.x, 2.0 / screenSize.y, -1.0, -1.0);
// vec4 screenTransform = vec4(1.0 / screenSize.x, 1.0 / screenSize.y, -0.5, -0.5);
vec2 position = clamp(vec2(interpolateAttribute(posX), interpolateAttribute(posY)), bounds.xy, bounds.zw);
float radius = interpolateAttribute(posR);
vec2 position = vec2(interpolateAttribute(posX), interpolateAttribute(posY)) + (radius * offset);
gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
float red = interpolateAttribute(colR);

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}};
@@ -18,8 +18,6 @@ export default class BlockScene {
animationOffset: number;
highlightingEnabled: boolean;
filterFlags: bigint | null = 0b00000100_00000000_00000000_00000000n;
x: number;
y: number;
width: number;
height: number;
gridWidth: number;
@@ -33,16 +31,14 @@ export default class BlockScene {
animateUntil = 0;
dirty: boolean;
constructor({ x = 0, y = 0, width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
{ x?: number, y?: number, width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
) {
this.init({ x, y,width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction });
this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction });
}
resize({ x = 0, y = 0, width = this.width, height = this.height, animate = true }: { x?: number, y?: number, width?: number, height?: number, animate: boolean }): void {
this.x = x;
this.y = y;
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
this.width = width;
this.height = height;
this.gridSize = this.width / this.gridWidth;
@@ -228,11 +224,7 @@ export default class BlockScene {
getTxAt(position: Position): TxView | void {
if (this.layout) {
const gridPosition = this.screenToGrid(position);
if (gridPosition.x >= 0 && gridPosition.x < this.gridWidth && gridPosition.y >= 0 && gridPosition.y < this.gridHeight) {
return this.layout.getTx(gridPosition);
} else {
return null;
}
return this.layout.getTx(gridPosition);
} else {
return null;
}
@@ -246,8 +238,8 @@ export default class BlockScene {
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
}
private init({ x, y, width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
{ x: number, y: number, width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
): void {
this.animationDuration = animationDuration || this.animationDuration || 1000;
@@ -272,7 +264,7 @@ export default class BlockScene {
this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.02, 2);
this.gridWidth = resolution;
this.gridHeight = resolution;
this.resize({ x, y, width, height, animate: true });
this.resize({ width, height, animate: true });
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
this.txs = {};
@@ -282,7 +274,7 @@ export default class BlockScene {
}
private applyTxUpdate(tx: TxView, update: ViewUpdateParams): void {
this.animateUntil = Math.max(this.animateUntil, tx.update(update, { minX: this.x, maxX: this.x + this.width, minY: this.y, maxY: this.y + this.height }));
this.animateUntil = Math.max(this.animateUntil, tx.update(update));
}
private updateTxColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration?: number): void {
@@ -398,7 +390,6 @@ export default class BlockScene {
position: {
x: tx.screenPosition.x + (direction === 'right' ? this.width + this.animationOffset : (direction === 'left' ? -this.width - this.animationOffset : 0)),
y: tx.screenPosition.y + (direction === 'up' ? this.height + this.animationOffset : (direction === 'down' ? -this.height - this.animationOffset : 0)),
s: tx.screenPosition.s
}
},
duration: this.animationDuration,
@@ -458,18 +449,18 @@ export default class BlockScene {
break;
}
return {
x: this.x + x + this.unitPadding - (slotSize / 2),
y: this.y + y + this.unitPadding - (slotSize / 2),
x: x + this.unitPadding - (slotSize / 2),
y: y + this.unitPadding - (slotSize / 2),
s: squareSize
};
} else {
return { x: this.x, y: this.y, s: 0 };
return { x: 0, y: 0, s: 0 };
}
}
private screenToGrid(position: Position): Position {
let x = position.x - this.x;
let y = position.y - this.y;
let x = position.x;
let y = this.height - position.y;
let t;
switch (this.orientation) {
@@ -926,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,14 +1,13 @@
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', 'r', 'g', 'b', 'a'];
const attributeKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];
const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];
export default class TxSprite {
static vertexSize = 28;
static vertexSize = 30;
static vertexCount = 6;
static dataSize: number = (28 * 6);
static dataSize: number = (30 * 6);
vertexArray: FastVertexArray;
vertexPointer: number;
@@ -17,26 +16,15 @@ export default class TxSprite {
attributes: Attributes;
tempAttributes: OptionalAttributes;
minX: number;
maxX: number;
minY: number;
maxY: number;
constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray, minX: number, maxX: number, minY: number, maxY: number) {
constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray) {
const offsetTime = params.start;
this.vertexArray = vertexArray;
this.vertexData = Array(TxSprite.dataSize).fill(0);
this.vertexData = Array(VI.length).fill(0);
this.updateMap = {
x: 0, y: 0, s: 0, r: 0, g: 0, b: 0, a: 0
};
this.minX = minX;
this.maxX = maxX;
this.minY = minY;
this.maxY = maxY;
this.attributes = {
x: { a: params.x, b: params.x, t: offsetTime, v: 0, d: 0 },
y: { a: params.y, b: params.y, t: offsetTime, v: 0, d: 0 },
@@ -89,24 +77,11 @@ export default class TxSprite {
minDuration: minimum remaining transition duration when adjust = true
temp: if true, this update is only temporary (can be reversed with 'resume')
*/
update(params: SpriteUpdateParams, minX?: number, maxX?: number, minY?: number, maxY?: number): void {
update(params: SpriteUpdateParams): void {
const offsetTime = params.start || performance.now();
const v = params.duration > 0 ? (1 / params.duration) : 0;
if (minX != null) {
this.minX = minX;
}
if (maxX != null) {
this.maxX = maxX;
}
if (minY != null) {
this.minY = minY;
}
if (maxY != null) {
this.maxY = maxY;
}
attributeKeys.forEach(key => {
updateKeys.forEach(key => {
this.updateMap[key] = params[key];
});
@@ -164,32 +139,18 @@ export default class TxSprite {
...this.tempAttributes
};
}
const size = attributes.s;
// update vertex data in place
// ugly, but avoids overhead of allocating large temporary arrays
const vertexStride = VI.length + 4;
const vertexStride = VI.length + 2;
for (let vertex = 0; vertex < 6; vertex++) {
this.vertexData[vertex * vertexStride] = this.minX;
this.vertexData[(vertex * vertexStride) + 1] = this.minY;
this.vertexData[(vertex * vertexStride) + 2] = this.maxX;
this.vertexData[(vertex * vertexStride) + 3] = this.maxY;
// x
this.vertexData[(vertex * vertexStride) + 4] = attributes[VI[0].a][VI[0].f] + (vertexOffsetFactors[vertex][0] * attributes.s.a);
this.vertexData[(vertex * vertexStride) + 5] = attributes[VI[1].a][VI[1].f] + (vertexOffsetFactors[vertex][0] * attributes.s.b);
this.vertexData[(vertex * vertexStride) + 6] = attributes[VI[2].a][VI[2].f];
this.vertexData[(vertex * vertexStride) + 7] = attributes[VI[3].a][VI[3].f];
// y
this.vertexData[(vertex * vertexStride) + 8] = attributes[VI[4].a][VI[4].f] + (vertexOffsetFactors[vertex][1] * attributes.s.a);
this.vertexData[(vertex * vertexStride) + 9] = attributes[VI[5].a][VI[5].f] + (vertexOffsetFactors[vertex][1] * attributes.s.b);
this.vertexData[(vertex * vertexStride) + 10] = attributes[VI[6].a][VI[6].f];
this.vertexData[(vertex * vertexStride) + 11] = attributes[VI[7].a][VI[7].f];
for (let step = 8; step < VI.length; step++) {
this.vertexData[vertex * vertexStride] = vertexOffsetFactors[vertex][0];
this.vertexData[(vertex * vertexStride) + 1] = vertexOffsetFactors[vertex][1];
for (let step = 0; step < VI.length; step++) {
// components of each field in the vertex array are defined by an entry in VI:
// VI[i].a is the attribute, VI[i].f is the inner field, VI[i].offA and VI[i].offB are offset factors
this.vertexData[(vertex * vertexStride) + step + 4] = attributes[VI[step].a][VI[step].f];
this.vertexData[(vertex * vertexStride) + step + 2] = attributes[VI[step].a][VI[step].f];
}
}

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');
@@ -106,7 +106,7 @@ export default class TxView implements TransactionStripped {
returns minimum transition end time
*/
update(params: ViewUpdateParams, { minX, maxX, minY, maxY }: { minX: number, maxX: number, minY: number, maxY: number }): number {
update(params: ViewUpdateParams): number {
if (params.jitter) {
params.delay += (Math.random() * params.jitter);
}
@@ -115,35 +115,21 @@ export default class TxView implements TransactionStripped {
this.initialised = true;
this.sprite = new TxSprite(
toSpriteUpdate(params),
this.vertexArray,
minX,
maxX,
minY,
maxY
this.vertexArray
);
// apply any pending hover event
if (this.hover) {
params.duration = Math.max(params.duration, hoverTransitionTime);
this.sprite.update(
{
...this.hoverColor,
duration: hoverTransitionTime,
adjust: false,
temp: true
},
minX,
maxX,
minY,
maxY
);
this.sprite.update({
...this.hoverColor,
duration: hoverTransitionTime,
adjust: false,
temp: true
});
}
} else {
this.sprite.update(
toSpriteUpdate(params),
minX,
maxX,
minY,
maxY
toSpriteUpdate(params)
);
}
this.dirty = false;

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,24 +0,0 @@
<div class="block-overview-graph">
<canvas *browserOnly class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
@if (!disableSpinner) {
<div class="loader-wrapper" [class.hidden]="!isLoading && !unavailable">
<div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div>
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
</div>
}
<app-block-overview-tooltip
[tx]="selectedTx || hoverTx"
[cursorPosition]="tooltipPosition"
[clickable]="!!selectedTx"
[auditEnabled]="auditHighlighting"
[blockConversion]="blockConversion"
[filterFlags]="activeFilterFlags"
[filterMode]="filterMode"
[relativeTime]="relativeTime"
></app-block-overview-tooltip>
<app-block-filters *ngIf="webGlEnabled && showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
<div *ngIf="!webGlEnabled" class="placeholder">
<span i18n="webgl-disabled">Your browser does not support this feature.</span>
</div>
</div>

View File

@@ -1,67 +0,0 @@
.block-overview-graph {
position: relative;
width: 100%;
height: 100%;
background: var(--stat-box-bg);
display: flex;
justify-content: center;
align-items: center;
grid-column: 1/-1;
.placeholder {
display: flex;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
height: 100%;
width: 100%;
align-items: center;
justify-content: center;
}
}
.graph-alignment {
position: relative;
width: 100%;
}
.grid-align {
display: grid;
grid-template-columns: repeat(auto-fit, 75px);
justify-content: center;
}
.block-overview-canvas {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
overflow: hidden;
&.clickable {
cursor: pointer;
}
}
.loader-wrapper {
position: absolute;
background: #181b2d7f;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
transition: opacity 500ms 500ms;
pointer-events: none;
&.hidden {
opacity: 0;
}
}

View File

@@ -1,803 +0,0 @@
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 '../block-overview-graph/fast-vertex-array';
import BlockScene from '../block-overview-graph/block-scene';
import TxSprite from '../block-overview-graph/tx-sprite';
import TxView from '../block-overview-graph/tx-view';
import { Color, Position } from '../block-overview-graph/sprite-types';
import { Price } from '../../services/price.service';
import { StateService } from '../../services/state.service';
import { ThemeService } from '../../services/theme.service';
import { Subscription } from 'rxjs';
import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction, contrastColorFunction, contrastAuditColors, contrastColors } from '../block-overview-graph/utils';
import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils';
import { detectWebGL } from '../../shared/graphs.utils';
const unmatchedOpacity = 0.2;
const unmatchedAuditColors = {
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
added_prioritized: setOpacity(defaultAuditColors.added_prioritized, unmatchedOpacity),
prioritized: setOpacity(defaultAuditColors.prioritized, unmatchedOpacity),
accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
};
const unmatchedContrastAuditColors = {
censored: setOpacity(contrastAuditColors.censored, unmatchedOpacity),
missing: setOpacity(contrastAuditColors.missing, unmatchedOpacity),
added: setOpacity(contrastAuditColors.added, unmatchedOpacity),
added_prioritized: setOpacity(contrastAuditColors.added_prioritized, unmatchedOpacity),
prioritized: setOpacity(contrastAuditColors.prioritized, unmatchedOpacity),
accelerated: setOpacity(contrastAuditColors.accelerated, unmatchedOpacity),
};
@Component({
selector: 'app-block-overview-multi',
templateUrl: './block-overview-multi.component.html',
styleUrls: ['./block-overview-multi.component.scss'],
})
export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, OnChanges {
@Input() isLoading: boolean;
@Input() resolution: number;
@Input() numBlocks: number;
@Input() padding: number = 0;
@Input() blockWidth: number = 360;
@Input() autofit: boolean = false;
@Input() blockLimit: number;
@Input() orientation = 'left';
@Input() flip = true;
@Input() animationDuration: number = 1000;
@Input() animationOffset: number | null = null;
@Input() disableSpinner = false;
@Input() mirrorTxid: string | void;
@Input() unavailable: boolean = false;
@Input() auditHighlighting: boolean = false;
@Input() showFilters: boolean = false;
@Input() excludeFilters: string[] = [];
@Input() filterFlags: bigint | null = null;
@Input() filterMode: FilterMode = 'and';
@Input() gradientMode: 'fee' | 'age' = 'fee';
@Input() relativeTime: number | null;
@Input() blockConversion: Price;
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
@Output() txHoverEvent = new EventEmitter<string>();
@Output() readyEvent = new EventEmitter();
@ViewChild('blockCanvas')
canvas: ElementRef<HTMLCanvasElement>;
themeChangedSubscription: Subscription;
gl: WebGLRenderingContext;
animationFrameRequest: number;
animationHeartBeat: number;
displayWidth: number;
displayHeight: number;
displayBlockWidth: number;
displayPadding: number;
cssWidth: number;
cssHeight: number;
shaderProgram: WebGLProgram;
vertexArray: FastVertexArray;
running: boolean;
scenes: BlockScene[] = [];
hoverTx: TxView | void;
selectedTx: TxView | void;
highlightTx: TxView | void;
mirrorTx: TxView | void;
tooltipPosition: Position;
readyNextFrame = false;
lastUpdate: number = 0;
pendingUpdates: {
count: number,
add: { [txid: string]: TransactionStripped },
remove: { [txid: string]: string },
change: { [txid: string]: { txid: string, rate: number | undefined, acc: boolean | undefined } },
direction?: string,
}[] = [];
searchText: string;
searchSubscription: Subscription;
filtersAvailable: boolean = true;
activeFilterFlags: bigint | null = null;
webGlEnabled = true;
constructor(
readonly ngZone: NgZone,
readonly elRef: ElementRef,
public stateService: StateService,
private themeService: ThemeService,
) {
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
}
ngAfterViewInit(): void {
if (this.canvas) {
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
this.gl = this.canvas.nativeElement.getContext('webgl');
this.initScenes();
if (this.gl) {
this.initCanvas();
this.resizeCanvas();
this.themeChangedSubscription = this.themeService.themeChanged$.subscribe(() => {
for (const scene of this.scenes) {
scene.setColorFunction(this.getColorFunction());
}
});
}
}
}
initScenes(): void {
for (const scene of this.scenes) {
if (scene) {
scene.destroy();
}
}
this.scenes = [];
this.pendingUpdates = [];
for (let i = 0; i < this.numBlocks; i++) {
this.scenes.push(null);
this.pendingUpdates.push({
count: 0,
add: {},
remove: {},
change: {},
direction: 'left',
});
}
this.resizeCanvas();
this.start();
}
ngOnChanges(changes): void {
if (changes.numBlocks) {
this.initScenes();
}
if (changes.orientation || changes.flip) {
for (const scene of this.scenes) {
scene?.setOrientation(this.orientation, this.flip);
}
}
if (changes.auditHighlighting) {
this.setHighlightingEnabled(this.auditHighlighting);
}
if (changes.overrideColor) {
for (const scene of this.scenes) {
scene?.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode));
}
}
if ((changes.filterFlags || changes.showFilters || changes.filterMode || changes.gradientMode)) {
this.setFilterFlags();
}
}
setFilterFlags(goggle?: ActiveFilter): void {
this.filterMode = goggle?.mode || this.filterMode;
this.gradientMode = goggle?.gradient || 'fee'; // this.gradientMode;
this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
for (const scene of this.scenes) {
if (scene) {
if (this.activeFilterFlags != null && this.filtersAvailable) {
scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode));
} else {
scene.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode));
}
}
}
this.start();
}
ngOnDestroy(): void {
if (this.animationFrameRequest) {
cancelAnimationFrame(this.animationFrameRequest);
clearTimeout(this.animationHeartBeat);
}
if (this.canvas) {
this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
this.themeChangedSubscription?.unsubscribe();
}
}
clear(block: number, direction): void {
this.exit(block, direction);
this.start();
}
destroy(block: number): void {
if (this.scenes[block]) {
this.scenes[block].destroy();
this.clearUpdateQueue(block);
this.start();
}
}
// initialize the scene without any entry transition
setup(block: number, transactions: TransactionStripped[], sort: boolean = false): void {
const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
if (filtersAvailable !== this.filtersAvailable) {
this.setFilterFlags();
}
this.filtersAvailable = filtersAvailable;
if (this.scenes[block]) {
this.clearUpdateQueue(block);
this.scenes[block].setup(transactions, sort);
this.readyNextFrame = true;
this.start();
}
}
enter(block: number, transactions: TransactionStripped[], direction: string): void {
if (this.scenes[block]) {
this.clearUpdateQueue(block);
this.scenes[block].enter(transactions, direction);
this.start();
}
}
exit(block: number, direction: string): void {
if (this.scenes[block]) {
this.clearUpdateQueue(block);
this.scenes[block].exit(direction);
this.start();
}
}
replace(block: number, transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
if (this.scenes[block]) {
this.clearUpdateQueue(block);
this.scenes[block].replace(transactions || [], direction, sort, startTime);
this.start();
}
}
// collates deferred updates into a set of consistent pending changes
queueUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
for (const tx of add) {
this.pendingUpdates[block].add[tx.txid] = tx;
delete this.pendingUpdates[block].remove[tx.txid];
delete this.pendingUpdates[block].change[tx.txid];
}
for (const txid of remove) {
delete this.pendingUpdates[block].add[txid];
this.pendingUpdates[block].remove[txid] = txid;
delete this.pendingUpdates[block].change[txid];
}
for (const tx of change) {
if (this.pendingUpdates[block].add[tx.txid]) {
this.pendingUpdates[block].add[tx.txid].rate = tx.rate;
this.pendingUpdates[block].add[tx.txid].acc = tx.acc;
} else {
this.pendingUpdates[block].change[tx.txid] = tx;
}
}
this.pendingUpdates[block].direction = direction;
this.pendingUpdates[block].count++;
}
deferredUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
this.queueUpdate(block, add, remove, change, direction);
this.applyQueuedUpdates();
}
applyQueuedUpdates(): void {
for (const [index, pendingUpdate] of this.pendingUpdates.entries()) {
if (pendingUpdate.count && performance.now() > (this.lastUpdate + this.animationDuration)) {
this.applyUpdate(index, Object.values(pendingUpdate.add), Object.values(pendingUpdate.remove), Object.values(pendingUpdate.change), pendingUpdate.direction);
this.clearUpdateQueue(index);
}
}
}
clearUpdateQueue(block: number): void {
this.pendingUpdates[block] = {
count: 0,
add: {},
remove: {},
change: {},
};
this.lastUpdate = performance.now();
}
update(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
// merge any pending changes into this update
this.queueUpdate(block, add, remove, change, direction);
this.applyUpdate(block,Object.values(this.pendingUpdates[block].add), Object.values(this.pendingUpdates[block].remove), Object.values(this.pendingUpdates[block].change), direction, resetLayout);
this.clearUpdateQueue(block);
}
applyUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
if (this.scenes[block]) {
add = add.filter(tx => !this.scenes[block].txs[tx.txid]);
remove = remove.filter(txid => this.scenes[block].txs[txid]);
change = change.filter(tx => this.scenes[block].txs[tx.txid]);
if (this.gradientMode === 'age') {
this.scenes[block].updateAllColors();
}
this.scenes[block].update(add, remove, change, direction, resetLayout);
this.start();
this.lastUpdate = performance.now();
}
}
initCanvas(): void {
if (!this.canvas || !this.gl) {
return;
}
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
const shaderSet = [
{
type: this.gl.VERTEX_SHADER,
src: vertShaderSrc
},
{
type: this.gl.FRAGMENT_SHADER,
src: fragShaderSrc
}
];
this.shaderProgram = this.buildShaderProgram(shaderSet);
this.gl.useProgram(this.shaderProgram);
// Set up alpha blending
this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
const glBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, glBuffer);
/* SET UP SHADER ATTRIBUTES */
Object.keys(attribs).forEach((key, i) => {
attribs[key].pointer = this.gl.getAttribLocation(this.shaderProgram, key);
this.gl.enableVertexAttribArray(attribs[key].pointer);
});
this.start();
}
handleContextLost(event): void {
event.preventDefault();
cancelAnimationFrame(this.animationFrameRequest);
this.animationFrameRequest = null;
this.running = false;
this.gl = null;
}
handleContextRestored(event): void {
if (this.canvas?.nativeElement) {
this.gl = this.canvas.nativeElement.getContext('webgl');
if (this.gl) {
this.initCanvas();
}
}
}
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
if (this.canvas) {
this.cssWidth = this.canvas.nativeElement.offsetParent.clientWidth;
this.cssHeight = this.canvas.nativeElement.offsetParent.clientHeight;
this.displayWidth = window.devicePixelRatio * this.cssWidth;
this.displayHeight = window.devicePixelRatio * this.cssHeight;
this.displayBlockWidth = window.devicePixelRatio * this.blockWidth;
this.displayPadding = window.devicePixelRatio * this.padding;
this.canvas.nativeElement.width = this.displayWidth;
this.canvas.nativeElement.height = this.displayHeight;
if (this.gl) {
this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
}
for (let i = 0; i < this.scenes.length; i++) {
const blocksPerRow = Math.floor(this.displayWidth / (this.displayBlockWidth + (this.displayPadding * 2)));
const x = this.displayPadding + ((i % blocksPerRow) * (this.displayBlockWidth + (this.displayPadding * 2)));
const numRows = Math.ceil(this.scenes.length / blocksPerRow);
const row = numRows - Math.floor(i / blocksPerRow) - 1;
const y = this.displayPadding + this.displayHeight - ((row + 1) * (this.displayBlockWidth + (this.displayPadding * 2)));
if (this.scenes[i]) {
this.scenes[i].resize({ x, y, width: this.displayBlockWidth, height: this.displayBlockWidth, animate: false });
this.start();
} else {
this.scenes[i] = new BlockScene({ x, y, width: this.displayBlockWidth, height: this.displayBlockWidth, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, theme: this.themeService,
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
colorFunction: this.getColorFunction() });
this.start();
}
}
}
}
compileShader(src, type): WebGLShader {
if (!this.gl) {
return;
}
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, src);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
console.log(`Error compiling ${type === this.gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:`);
console.log(this.gl.getShaderInfoLog(shader));
}
return shader;
}
buildShaderProgram(shaderInfo): WebGLProgram {
if (!this.gl) {
return;
}
const program = this.gl.createProgram();
shaderInfo.forEach((desc) => {
const shader = this.compileShader(desc.src, desc.type);
if (shader) {
this.gl.attachShader(program, shader);
}
});
this.gl.linkProgram(program);
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
console.log('Error linking shader program:');
console.log(this.gl.getProgramInfoLog(program));
}
return program;
}
start(): void {
this.running = true;
this.ngZone.runOutsideAngular(() => this.doRun());
}
doRun(): void {
if (this.animationFrameRequest) {
cancelAnimationFrame(this.animationFrameRequest);
}
this.animationFrameRequest = requestAnimationFrame(() => this.run());
}
run(now?: DOMHighResTimeStamp): void {
if (!now) {
now = performance.now();
}
this.applyQueuedUpdates();
// skip re-render if there's no change to the scene
if (this.scenes.length && this.gl) {
/* SET UP SHADER UNIFORMS */
// screen dimensions
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
// frame timestamp
this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, 'now'), now);
if (this.vertexArray.dirty) {
/* SET UP SHADER ATTRIBUTES */
Object.keys(attribs).forEach((key, i) => {
this.gl.vertexAttribPointer(attribs[key].pointer,
attribs[key].count, // number of primitives in this attribute
this.gl[attribs[key].type], // type of primitive in this attribute (e.g. gl.FLOAT)
false, // never normalised
stride, // distance between values of the same attribute
attribs[key].offset); // offset of the first value
});
const pointArray = this.vertexArray.getVertexData();
if (pointArray.length) {
this.gl.bufferData(this.gl.ARRAY_BUFFER, pointArray, this.gl.DYNAMIC_DRAW);
this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
}
this.vertexArray.dirty = false;
} else {
const pointArray = this.vertexArray.getVertexData();
if (pointArray.length) {
this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
}
}
if (this.readyNextFrame) {
this.readyNextFrame = false;
this.readyEvent.emit();
}
}
/* LOOP */
if (this.running && this.scenes.length && now <= (this.scenes.reduce((max, scene) => scene.animateUntil > max ? scene.animateUntil : max, 0) + 500)) {
this.doRun();
} else {
if (this.animationHeartBeat) {
clearTimeout(this.animationHeartBeat);
}
this.animationHeartBeat = window.setTimeout(() => {
this.start();
}, 1000);
}
}
@HostListener('document:click', ['$event'])
clickAway(event) {
if (!this.elRef.nativeElement.contains(event.target)) {
const currentPreview = this.selectedTx || this.hoverTx;
if (currentPreview) {
for (const scene of this.scenes) {
if (scene) {
scene.setHover(currentPreview, false);
}
}
this.start();
}
this.hoverTx = null;
this.selectedTx = null;
this.onTxHover(null);
}
}
@HostListener('pointerup', ['$event'])
onClick(event) {
if (!this.canvas) {
return;
}
if (event.target === this.canvas.nativeElement && event.pointerType === 'touch') {
this.setPreviewTx(event.offsetX, event.offsetY, true);
} else if (event.target === this.canvas.nativeElement) {
const keyMod = event.shiftKey || event.ctrlKey || event.metaKey;
const middleClick = event.which === 2 || event.button === 1;
this.onTxClick(event.offsetX, event.offsetY, keyMod || middleClick);
}
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
if (!this.canvas) {
return;
}
if (event.target === this.canvas.nativeElement) {
this.setPreviewTx(event.offsetX, event.offsetY, false);
} else {
this.onPointerLeave(event);
}
}
@HostListener('pointerleave', ['$event'])
onPointerLeave(event) {
if (event.pointerType !== 'touch') {
this.setPreviewTx(-1, -1, true);
}
}
setPreviewTx(cssX: number, cssY: number, clicked: boolean = false) {
const x = cssX * window.devicePixelRatio;
const y = cssY * window.devicePixelRatio;
if (!this.selectedTx || clicked) {
this.tooltipPosition = {
x: cssX,
y: cssY
};
const currentPreview = this.selectedTx || this.hoverTx;
let selected;
for (const scene of this.scenes) {
if (scene) {
selected = scene.getTxAt({ x, y: this.displayHeight - y });
if (selected) {
break;
}
}
}
if (selected !== currentPreview) {
if (currentPreview) {
for (const scene of this.scenes) {
if (scene) {
scene.setHover(currentPreview, false);
break;
}
}
this.start();
}
if (selected) {
for (const scene of this.scenes) {
if (scene) {
scene.setHover(selected, true);
break;
}
}
this.start();
if (clicked) {
this.selectedTx = selected;
} else {
this.hoverTx = selected;
this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
}
} else {
if (clicked) {
this.selectedTx = null;
}
this.hoverTx = null;
this.onTxHover(null);
}
} else if (clicked) {
if (selected === this.selectedTx) {
this.hoverTx = this.selectedTx;
this.selectedTx = null;
this.onTxHover(this.hoverTx ? this.hoverTx.txid : null);
} else {
this.selectedTx = selected;
}
}
}
}
updateSearchHighlight(): void {
if (this.highlightTx && this.highlightTx.txid !== this.searchText) {
for (const scene of this.scenes) {
if (scene) {
scene.setHighlight(this.highlightTx, false);
}
}
this.start();
} else if (this.searchText && this.searchText.length === 64) {
for (const scene of this.scenes) {
if (scene) {
const highlightTx = scene.txs[this.searchText];
if (highlightTx) {
scene.setHighlight(highlightTx, true);
this.highlightTx = highlightTx;
this.start();
}
}
}
}
}
setHighlightingEnabled(enabled: boolean): void {
for (const scene of this.scenes) {
scene.setHighlighting(enabled);
}
this.start();
}
onTxClick(cssX: number, cssY: number, keyModifier: boolean = false) {
for (const scene of this.scenes) {
if (scene) {
const x = cssX * window.devicePixelRatio;
const y = cssY * window.devicePixelRatio;
const selected = scene.getTxAt({ x, y: this.displayHeight - y });
if (selected && selected.txid) {
this.txClickEvent.emit({ tx: selected, keyModifier });
return;
}
}
}
}
onTxHover(hoverId: string) {
this.txHoverEvent.emit(hoverId);
}
getColorFunction(): ((tx: TxView) => Color) {
if (this.overrideColors) {
return this.overrideColors;
} else if (this.filterFlags) {
return this.getFilterColorFunction(this.filterFlags, this.gradientMode);
} else if (this.activeFilterFlags) {
return this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode);
} else {
return this.getFilterColorFunction(0n, this.gradientMode);
}
}
getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) {
return (tx: TxView) => {
if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
return (gradient === 'age') ? ageColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)) : defaultColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000));
} else {
return (gradient === 'age') ? ageColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)) : contrastColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000));
}
} else {
if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : defaultColorFunction(
tx,
defaultColors.unmatchedfee,
unmatchedAuditColors,
this.relativeTime || (Date.now() / 1000)
);
} else {
return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : contrastColorFunction(
tx,
contrastColors.unmatchedfee,
unmatchedContrastAuditColors,
this.relativeTime || (Date.now() / 1000)
);
}
}
};
}
}
// WebGL shader attributes
const attribs = {
bounds: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
posY: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colG: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colA: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }
};
// Calculate the number of bytes per vertex based on specified attributes
const stride = Object.values(attribs).reduce((total, attrib) => {
return total + (attrib.count * 4);
}, 0);
// Calculate vertex attribute offsets
for (let i = 0, offset = 0; i < Object.keys(attribs).length; i++) {
const attrib = Object.values(attribs)[i];
attrib.offset = offset;
offset += (attrib.count * 4);
}
const vertShaderSrc = `
varying lowp vec4 vColor;
// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
// shader interpolates between start and end values at the given rate, from the given time
attribute vec4 bounds;
attribute vec4 posX;
attribute vec4 posY;
attribute vec4 colR;
attribute vec4 colG;
attribute vec4 colB;
attribute vec4 colA;
uniform vec2 screenSize;
uniform float now;
float smootherstep(float x) {
x = clamp(x, 0.0, 1.0);
float ix = 1.0 - x;
x = x * x;
return x / (x + ix * ix);
}
float interpolateAttribute(vec4 attr) {
float d = (now - attr.z) * attr.w;
float delta = smootherstep(d);
return mix(attr.x, attr.y, delta);
}
void main() {
vec4 screenTransform = vec4(2.0 / screenSize.x, 2.0 / screenSize.y, -1.0, -1.0);
// vec4 screenTransform = vec4(1.0 / screenSize.x, 1.0 / screenSize.y, -0.5, -0.5);
vec2 position = clamp(vec2(interpolateAttribute(posX), interpolateAttribute(posY)), bounds.xy, bounds.zw);
gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
float red = interpolateAttribute(colR);
float green = interpolateAttribute(colG);
float blue = interpolateAttribute(colB);
float alpha = interpolateAttribute(colA);
vColor = vec4(red, green, blue, alpha);
}
`;
const fragShaderSrc = `
varying lowp vec4 vColor;
void main() {
gl_FragColor = vColor;
// premultiply alpha
gl_FragColor.rgb *= gl_FragColor.a;
}
`;

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,13 @@ 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';
@Component({
selector: 'app-blocks-list',

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

@@ -1,24 +1,23 @@
<app-block-overview-multi
#blockGraph
[isLoading]="isLoadingTransactions"
[numBlocks]="numBlocks"
[padding]="padding"
[blockWidth]="blockWidth"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="'top'"
[flip]="false"
[animationDuration]="animationDuration"
[animationOffset]="animationOffset"
></app-block-overview-multi>
<div class="blocks" [class.wrap]="wrapBlocks">
<ng-container *ngFor="let i of blockIndices">
<div class="block-wrapper" [style]="wrapperStyle">
<div class="block-container" [style]="containerStyle">
<app-block-overview-graph
#blockGraph
[isLoading]="false"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="'top'"
[flip]="false"
[animationDuration]="animationDuration"
[animationOffset]="animationOffset"
[disableSpinner]="true"
[relativeTime]="blockInfo[i]?.timestamp"
(txClickEvent)="onTxClick($event)"
></app-block-overview-graph>
<div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
<h1 class="height">{{ blockInfo[i].height }}</h1>
<h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }} <img class="pool-logo" [src]="'/resources/mining-pools/' + blockInfo[i].extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'"> </h2>
<h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }}</h2>
</div>
</div>
</div>

View File

@@ -1,7 +1,4 @@
.blocks {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
min-width: 100vw;
@@ -69,12 +66,4 @@
.block-container {
overflow: hidden;
}
}
.pool-logo {
width: 1.2em;
height: 1.2em;
position: relative;
top: -1px;
margin-right: 2px;
}

View File

@@ -1,16 +1,16 @@
import { Component, OnInit, OnDestroy, ViewChild, HostListener } from '@angular/core';
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { catchError } from 'rxjs/operators';
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 { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
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 { BlockOverviewMultiComponent } from '../block-overview-multi/block-overview-multi.component';
import { CacheService } from '../../services/cache.service';
import { BytesPipe } from '@app/shared/pipes/bytes-pipe/bytes.pipe';
function bestFitResolution(min, max, n): number {
const target = (min + max) / 2;
@@ -48,27 +48,24 @@ interface BlockInfo extends BlockExtended {
})
export class EightBlocksComponent implements OnInit, OnDestroy {
network = '';
latestBlocks: (BlockExtended | null)[] = [];
pendingBlocks: Record<number, ((b: BlockExtended) => void)[]> = {};
latestBlocks: BlockExtended[] = [];
isLoadingTransactions = true;
strippedTransactions: { [height: number]: TransactionStripped[] } = {};
webGlEnabled = true;
hoverTx: string | null = null;
tipSubscription: Subscription;
blocksSubscription: Subscription;
cacheBlocksSubscription: Subscription;
networkChangedSubscription: Subscription;
queryParamsSubscription: Subscription;
graphChangeSubscription: Subscription;
height: number = 0;
numBlocks: number = 8;
autoNumBlocks: boolean = false;
blockIndices: number[] = [...Array(8).keys()];
autofit: boolean = false;
padding: number = 0;
wrapBlocks: boolean = false;
blockWidth: number = 360;
blockWidth: number = 1080;
animationDuration: number = 2000;
animationOffset: number = 0;
stagger: number = 0;
@@ -82,14 +79,13 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
wrapperStyle = {
'--block-width': '1080px',
width: '1080px',
height: '1080px',
maxWidth: '1080px',
margin: '',
padding: '',
};
containerStyle = {};
resolution: number = 86;
@ViewChild('blockGraph') blockGraph: BlockOverviewMultiComponent;
@ViewChildren('blockGraph') blockGraphs: QueryList<BlockOverviewGraphComponent>;
constructor(
private route: ActivatedRoute,
@@ -97,7 +93,6 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
public stateService: StateService,
private websocketService: WebsocketService,
private apiService: ApiService,
private cacheService: CacheService,
private bytesPipe: BytesPipe,
) {
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
@@ -108,24 +103,15 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
this.network = this.stateService.network;
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 8;
this.blockIndices = [...Array(this.numBlocks).keys()];
this.autofit = params.autofit !== 'false';
this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 0;
this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 320;
this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 4;
this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 10;
this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 540;
this.wrapBlocks = params.wrap !== 'false';
this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
this.animationOffset = 0;
if (!this.numBlocks) {
this.autoNumBlocks = true;
const width = window.innerWidth;
const height = window.innerHeight;
const paddedWidth = this.blockWidth + (this.padding * 2);
this.numBlocks = Math.floor(width / paddedWidth) * Math.floor(height / paddedWidth);
}
this.blockIndices = [...Array(this.numBlocks).keys()];
this.animationOffset = this.padding * 2;
if (this.autofit) {
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
@@ -136,24 +122,24 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
this.wrapperStyle = {
'--block-width': this.blockWidth + 'px',
width: this.blockWidth + 'px',
height: this.blockWidth + 'px',
maxWidth: this.blockWidth + 'px',
margin: (this.padding || 0) +'px ',
padding: (this.padding || 0) +'px 0px',
};
this.cacheBlocksSubscription = this.cacheService.loadedBlocks$.subscribe((block: BlockExtended) => {
if (this.pendingBlocks[block.height]) {
this.pendingBlocks[block.height].forEach(resolve => resolve(block));
delete this.pendingBlocks[block.height];
if (params.test === 'true') {
if (this.blocksSubscription) {
this.blocksSubscription.unsubscribe();
}
});
this.tipSubscription?.unsubscribe();
this.tipSubscription = this.stateService.chainTip$
.subscribe((height) => {
this.height = height;
this.handleNewBlock(height);
this.blocksSubscription = (new Subject<BlockExtended[]>()).subscribe((blocks) => {
this.handleNewBlock(blocks.slice(0, this.numBlocks));
});
this.shiftTestBlocks();
} else if (!this.blocksSubscription) {
this.blocksSubscription = this.stateService.blocks$
.subscribe((blocks) => {
this.handleNewBlock(blocks.slice(0, this.numBlocks));
});
}
});
this.setupBlockGraphs();
@@ -163,80 +149,53 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
}
ngAfterViewInit(): void {
this.setupBlockGraphs();
}
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
if (this.autoNumBlocks) {
this.autoNumBlocks = true;
const width = window.innerWidth;
const height = window.innerHeight;
const paddedWidth = this.blockWidth + (this.padding * 2);
this.numBlocks = Math.floor(width / paddedWidth) * Math.floor(height / paddedWidth);
this.blockIndices = [...Array(this.numBlocks).keys()];
if (this.autofit) {
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
} else {
this.resolution = 86;
}
this.wrapperStyle = {
'--block-width': this.blockWidth + 'px',
width: this.blockWidth + 'px',
height: this.blockWidth + 'px',
maxWidth: this.blockWidth + 'px',
margin: (this.padding || 0) +'px ',
};
if (this.cacheBlocksSubscription) {
this.cacheBlocksSubscription.unsubscribe();
}
this.cacheBlocksSubscription = this.cacheService.loadedBlocks$.subscribe((block: BlockExtended) => {
if (this.pendingBlocks[block.height]) {
this.pendingBlocks[block.height].forEach(resolve => resolve(block));
delete this.pendingBlocks[block.height];
}
});
this.tipSubscription?.unsubscribe();
this.tipSubscription = this.stateService.chainTip$
.subscribe((height) => {
this.height = height;
this.handleNewBlock(height);
});
this.graphChangeSubscription = this.blockGraphs.changes.pipe(startWith(null)).subscribe(() => {
this.setupBlockGraphs();
}
});
}
ngOnDestroy(): void {
this.stateService.markBlock$.next({});
if (this.tipSubscription) {
this.tipSubscription?.unsubscribe();
if (this.blocksSubscription) {
this.blocksSubscription?.unsubscribe();
}
this.cacheBlocksSubscription?.unsubscribe();
this.networkChangedSubscription?.unsubscribe();
this.queryParamsSubscription?.unsubscribe();
}
async handleNewBlock(height: number): Promise<void> {
shiftTestBlocks(): void {
const sub = this.apiService.getBlocks$(this.testHeight).subscribe(result => {
sub.unsubscribe();
this.handleNewBlock(result.slice(0, this.numBlocks));
this.testHeight++;
clearTimeout(this.testShiftTimeout);
this.testShiftTimeout = window.setTimeout(() => { this.shiftTestBlocks(); }, 10000);
});
}
async handleNewBlock(blocks: BlockExtended[]): Promise<void> {
const readyPromises: Promise<TransactionStripped[]>[] = [];
const previousBlocks = this.latestBlocks;
const blocks = await this.loadBlocks(height, this.numBlocks);
const newHeights = {};
this.latestBlocks = blocks;
for (const block of blocks) {
newHeights[block.height] = true;
if (!this.strippedTransactions[block.height]) {
readyPromises.push(this.loadBlockTransactions(block));
readyPromises.push(new Promise((resolve) => {
const subscription = this.apiService.getStrippedBlockTransactions$(block.id).pipe(
catchError(() => {
return of([]);
}),
).subscribe((transactions) => {
this.strippedTransactions[block.height] = transactions;
subscription.unsubscribe();
resolve(transactions);
});
}));
}
}
await Promise.allSettled(readyPromises);
this.isLoadingTransactions = false;
this.updateBlockGraphs(blocks);
// free up old transactions
@@ -247,44 +206,12 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
});
}
async loadBlocks(height: number, numBlocks: number): Promise<BlockExtended[]> {
const promises: Promise<BlockExtended>[] = [];
for (let i = 0; i < numBlocks; i++) {
this.cacheService.loadBlock(height - i);
const cachedBlock = this.cacheService.getCachedBlock(height - i);
if (cachedBlock) {
promises.push(Promise.resolve(cachedBlock));
} else {
promises.push(new Promise((resolve) => {
if (!this.pendingBlocks[height - i]) {
this.pendingBlocks[height - i] = [];
}
this.pendingBlocks[height - i].push(resolve);
}));
}
}
return Promise.all(promises);
}
async loadBlockTransactions(block: BlockExtended): Promise<TransactionStripped[]> {
return new Promise((resolve) => {
this.apiService.getStrippedBlockTransactions$(block.id).pipe(
catchError(() => {
return of([]);
}),
).subscribe((transactions) => {
this.strippedTransactions[block.height] = transactions;
resolve(transactions);
});
});
}
updateBlockGraphs(blocks): void {
const startTime = performance.now() + 1000 - (this.stagger < 0 ? this.stagger * 8 : 0);
if (this.blockGraph) {
for (let i = 0; i < this.numBlocks; i++) {
this.blockGraph.replace(i, this.strippedTransactions[blocks?.[this.getBlockIndex(i)]?.height] || [], 'right', false, startTime + (this.stagger * i));
}
if (this.blockGraphs) {
this.blockGraphs.forEach((graph, index) => {
graph.replace(this.strippedTransactions[blocks?.[index]?.height] || [], 'right', false, startTime + (this.stagger * index));
});
}
this.showInfo = false;
setTimeout(() => {
@@ -299,22 +226,28 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
}
setupBlockGraphs(): void {
if (this.blockGraph) {
for (let i = 0; i < this.numBlocks; i++) {
this.blockGraph.destroy(i);
this.blockGraph.setup(i, this.strippedTransactions[this.latestBlocks?.[this.getBlockIndex(i)]?.height] || []);
}
if (this.blockGraphs) {
this.blockGraphs.forEach((graph, index) => {
graph.destroy();
graph.setup(this.strippedTransactions[this.latestBlocks?.[index]?.height] || []);
});
}
}
getBlockIndex(slotIndex: number): number {
const width = window.innerWidth;
const height = window.innerHeight;
const paddedWidth = this.blockWidth + (this.padding * 2);
const blocksPerRow = Math.floor(width / paddedWidth);
const blocksPerColumn = Math.floor(height / paddedWidth);
const row = Math.floor(slotIndex / blocksPerRow);
const column = slotIndex % blocksPerRow;
return (blocksPerColumn - 1 - row) * blocksPerRow + column;
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
if (!event.keyModifier) {
this.router.navigate([url]);
} else {
window.open(url, '_blank');
}
}
onTxHover(txid: string): void {
if (txid && txid.length) {
this.hoverTx = txid;
} else {
this.hoverTx = null;
}
}
}

View File

@@ -1,15 +0,0 @@
<app-block-overview-multi
#blockGraph
[isLoading]="isLoading"
[numBlocks]="numBlocks"
[padding]="padding"
[blockWidth]="blockWidth"
[resolution]="resolution"
[blockLimit]="stateService.blockVSize"
[orientation]="'left'"
[flip]="true"
[showFilters]="true"
[animationDuration]="animationDuration"
[animationOffset]="animationOffset"
(txClickEvent)="onTxClick($event)"
></app-block-overview-multi>

View File

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

View File

@@ -1,249 +0,0 @@
import { Component, OnInit, OnDestroy, ViewChild, HostListener } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { ApiService } from '../../services/api.service';
import { detectWebGL } from '../../shared/graphs.utils';
import { animate, style, transition, trigger } from '@angular/animations';
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
import { BlockOverviewMultiComponent } from '../block-overview-multi/block-overview-multi.component';
import { CacheService } from '../../services/cache.service';
import { isMempoolDelta, MempoolBlockDelta } from '../../interfaces/websocket.interface';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
function bestFitResolution(min, max, n): number {
const target = (min + max) / 2;
let bestScore = Infinity;
let best = null;
for (let i = min; i <= max; i++) {
const remainder = (n % i);
if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
bestScore = remainder;
best = i;
}
}
return best;
}
@Component({
selector: 'app-eight-mempool',
templateUrl: './eight-mempool.component.html',
styleUrls: ['./eight-mempool.component.scss'],
animations: [
trigger('infoChange', [
transition(':enter', [
style({ opacity: 0 }),
animate('1000ms', style({ opacity: 1 })),
]),
transition(':leave', [
animate('1000ms 500ms', style({ opacity: 0 }))
])
]),
],
})
export class EightMempoolComponent implements OnInit, OnDestroy {
network = '';
strippedTransactions: { [height: number]: TransactionStripped[] } = {};
isLoading = true;
webGlEnabled = true;
hoverTx: string | null = null;
networkChangedSubscription: Subscription;
queryParamsSubscription: Subscription;
graphChangeSubscription: Subscription;
blockSub: Subscription;
chainDirection: string = 'right';
poolDirection: string = 'left';
lastBlockHeight: number = 0;
lastBlockHeightUpdate: number[] = [];
numBlocks: number = 8;
autoNumBlocks: boolean = false;
blockIndices: number[] = [];
autofit: boolean = false;
padding: number = 0;
wrapBlocks: boolean = false;
blockWidth: number = 360;
animationDuration: number = 2000;
animationOffset: number = 0;
stagger: number = 0;
testing: boolean = true;
testHeight: number = 800000;
testShiftTimeout: number;
showInfo: boolean = true;
wrapperStyle = {
'--block-width': '1080px',
width: '1080px',
height: '1080px',
maxWidth: '1080px',
margin: '',
};
containerStyle = {};
resolution: number = 86;
@ViewChild('blockGraph') blockGraph: BlockOverviewMultiComponent;
constructor(
private route: ActivatedRoute,
private router: Router,
public stateService: StateService,
private websocketService: WebsocketService,
private apiService: ApiService,
private cacheService: CacheService,
private bytesPipe: BytesPipe,
) {
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
}
ngOnInit(): void {
this.websocketService.want(['blocks', 'mempool-blocks']);
this.network = this.stateService.network;
this.stateService.activeGoggles$.next({ mode: 'and', filters: [], gradient: 'fee' });
this.blockSub = this.stateService.mempoolBlockUpdate$.subscribe((update) => {
// process update
if (isMempoolDelta(update)) {
// delta
this.updateBlock(update);
} else {
const transactionsStripped = update.transactions;
const inOldBlock = {};
const inNewBlock = {};
const added: TransactionStripped[] = [];
const changed: { txid: string, rate: number | undefined, flags: number, acc: boolean | undefined }[] = [];
const removed: string[] = [];
for (const tx of transactionsStripped) {
inNewBlock[tx.txid] = true;
}
for (const txid of Object.keys(this.blockGraph?.scenes[this.numBlocks - update.block - 1]?.txs || {})) {
inOldBlock[txid] = true;
if (!inNewBlock[txid]) {
removed.push(txid);
}
}
for (const tx of transactionsStripped) {
if (!inOldBlock[tx.txid]) {
added.push(tx);
} else {
changed.push({
txid: tx.txid,
rate: tx.rate,
flags: tx.flags,
acc: tx.acc
});
}
}
this.updateBlock({
block: update.block,
removed,
changed,
added
});
}
});
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
this.autofit = params.autofit !== 'false';
this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 0;
this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 320;
this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 4;
this.wrapBlocks = params.wrap !== 'false';
this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
this.animationOffset = 0;
if (!this.numBlocks) {
this.autoNumBlocks = true;
const width = window.innerWidth;
const height = window.innerHeight;
const paddedWidth = this.blockWidth + (this.padding * 2);
this.numBlocks = Math.floor(width / paddedWidth) * Math.floor(height / paddedWidth);
}
this.blockIndices = [...Array(this.numBlocks).keys()];
this.lastBlockHeightUpdate = this.blockIndices.map(() => 0);
if (this.autofit) {
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
} else {
this.resolution = 86;
}
this.wrapperStyle = {
'--block-width': this.blockWidth + 'px',
width: this.blockWidth + 'px',
height: this.blockWidth + 'px',
maxWidth: this.blockWidth + 'px',
margin: (this.padding || 0) +'px ',
};
this.websocketService.startTrackMempoolBlocks(this.blockIndices);
});
this.networkChangedSubscription = this.stateService.networkChanged$
.subscribe((network) => this.network = network);
}
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
if (!event.keyModifier) {
this.router.navigate([url]);
} else {
window.open(url, '_blank');
}
}
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
if (this.autoNumBlocks) {
this.autoNumBlocks = true;
const width = window.innerWidth;
const height = window.innerHeight;
const paddedWidth = this.blockWidth + (this.padding * 2);
this.numBlocks = Math.floor(width / paddedWidth) * Math.floor(height / paddedWidth);
this.blockIndices = [...Array(this.numBlocks).keys()];
this.lastBlockHeightUpdate = this.blockIndices.map(() => 0);
if (this.autofit) {
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
} else {
this.resolution = 86;
}
this.wrapperStyle = {
'--block-width': this.blockWidth + 'px',
width: this.blockWidth + 'px',
height: this.blockWidth + 'px',
maxWidth: this.blockWidth + 'px',
margin: (this.padding || 0) +'px ',
};
this.websocketService.startTrackMempoolBlocks(this.blockIndices);
}
}
ngOnDestroy(): void {
this.stateService.markBlock$.next({});
this.blockSub.unsubscribe();
this.networkChangedSubscription?.unsubscribe();
this.queryParamsSubscription?.unsubscribe();
}
updateBlock(delta: MempoolBlockDelta): void {
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeightUpdate[delta.block]);
if (blockMined) {
this.blockGraph.update(this.numBlocks - delta.block - 1, delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined);
} else {
this.blockGraph.update(this.numBlocks - delta.block - 1, delta.added, delta.removed, delta.changed || [], this.poolDirection);
}
this.isLoading = false;
this.lastBlockHeightUpdate[delta.block] = this.stateService.latestBlockHeight;
}
}

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',

View File

@@ -1,9 +1,9 @@
import { HostListener, OnChanges, OnDestroy } from '@angular/core';
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { StateService } from '../../services/state.service';
import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe';
import { selectPowerOfTen } from '../../bitcoin.utils';
import { TransactionStripped } from '@interfaces/node-api.interface';
import { StateService } from '@app/services/state.service';
import { VbytesPipe } from '@app/shared/pipes/bytes-pipe/vbytes.pipe';
import { selectPowerOfTen } from '@app/bitcoin.utils';
import { Subscription } from 'rxjs';
@Component({

View File

@@ -1,10 +1,10 @@
import { Component, OnInit, ChangeDetectionStrategy, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { StateService } from '../../services/state.service';
import { StateService } from '@app/services/state.service';
import { Observable, combineLatest, Subscription } from 'rxjs';
import { Recommendedfees } from '../../interfaces/websocket.interface';
import { feeLevels } from '../../app.constants';
import { Recommendedfees } from '@interfaces/websocket.interface';
import { feeLevels } from '@app/app.constants';
import { map, startWith, tap } from 'rxjs/operators';
import { ThemeService } from '../../services/theme.service';
import { ThemeService } from '@app/services/theme.service';
@Component({
selector: 'app-fees-box',

View File

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

View File

@@ -1,8 +1,8 @@
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
import { StateService } from '../../services/state.service';
import { StateService } from '@app/services/state.service';
import { Observable, combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { MempoolInfo } from '../../interfaces/websocket.interface';
import { MempoolInfo } from '@interfaces/websocket.interface';
interface MempoolBlocksData {
blocks: number;

View File

@@ -1,6 +1,8 @@
import { Component, OnInit } from '@angular/core';
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';
import { Router, ActivatedRoute } from '@angular/router';
import { handleDemoRedirect } from '../../shared/common.utils';
@Component({
selector: 'app-graphs',
@@ -13,7 +15,9 @@ export class GraphsComponent implements OnInit {
constructor(
public stateService: StateService,
private websocketService: WebsocketService
private websocketService: WebsocketService,
private router: Router,
private route: ActivatedRoute
) { }
ngOnInit(): void {
@@ -22,5 +26,7 @@ export class GraphsComponent implements OnInit {
if (this.stateService.env.ACCELERATOR === true && (this.stateService.env.MINING_DASHBOARD === true || this.stateService.env.LIGHTNING === true)) {
this.flexWrap = true;
}
handleDemoRedirect(this.route, this.router);
}
}

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