Compare commits
117 Commits
mononaut/m
...
natsoni/am
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
865015a193 | ||
|
|
a0596cd366 | ||
|
|
0f14aa7ad3 | ||
|
|
44a0f92cc1 | ||
|
|
97a9ea47fc | ||
|
|
d573147ad4 | ||
|
|
679745fb6c | ||
|
|
c089920e4b | ||
|
|
62c96272d8 | ||
|
|
ebd4170da1 | ||
|
|
0310452dfb | ||
|
|
969687ef39 | ||
|
|
0831256cce | ||
|
|
af40cac284 | ||
|
|
e4868b70c1 | ||
|
|
5f45ce80f1 | ||
|
|
14e49126c3 | ||
|
|
a4d73130b7 | ||
|
|
cd702955fc | ||
|
|
4c66bf61f0 | ||
|
|
ffa582558b | ||
|
|
9a81db8e6c | ||
|
|
cb3326d691 | ||
|
|
535e5313ef | ||
|
|
8bd6d40ed2 | ||
|
|
7516db0c71 | ||
|
|
abe9aa1fdc | ||
|
|
60b3f9ace6 | ||
|
|
073fe8e8cd | ||
|
|
bfe7b996a4 | ||
|
|
bfd771056d | ||
|
|
e05f5ee751 | ||
|
|
d9f3611da3 | ||
|
|
7c7419ab1c | ||
|
|
96afbca029 | ||
|
|
8c80358e71 | ||
|
|
a5fbc94182 | ||
|
|
fd7f340854 | ||
|
|
7f784944af | ||
|
|
cab01f7f26 | ||
|
|
3b4eda432f | ||
|
|
8b699da721 | ||
|
|
5b2f613856 | ||
|
|
f1e2c893cc | ||
|
|
7d3d59c348 | ||
|
|
8719b424e5 | ||
|
|
ef498b55ed | ||
|
|
9718610104 | ||
|
|
937e82bb89 | ||
|
|
91bf35bb65 | ||
|
|
f0f6ee1919 | ||
|
|
7b837b96da | ||
|
|
c417470be2 | ||
|
|
dfd7877f82 | ||
|
|
136e80e5cf | ||
|
|
b3aed2f58b | ||
|
|
e75f913af3 | ||
|
|
0a9703f164 | ||
|
|
db321c3fa5 | ||
|
|
b6aeb5661f | ||
|
|
f08fa034cc | ||
|
|
c0ef01d4da | ||
|
|
c6711d8191 | ||
|
|
216bd5ad23 | ||
|
|
a7d59d6b2e | ||
|
|
a257bcc12a | ||
|
|
66c0ea7ca3 | ||
|
|
7692b2af66 | ||
|
|
397f53f42d | ||
|
|
2ea76d9c38 | ||
|
|
59ac27b104 | ||
|
|
d27bb7e156 | ||
|
|
4eadfc0a3b | ||
|
|
76cfa3ca47 | ||
|
|
3a4a4d9ffd | ||
|
|
8b01a83948 | ||
|
|
2ceb9001a1 | ||
|
|
d44b7926d2 | ||
|
|
57299e086e | ||
|
|
c1d17dac43 | ||
|
|
cb49f9d929 | ||
|
|
185be3d598 | ||
|
|
aa9888a2fe | ||
|
|
c950e3d0ae | ||
|
|
3909148d6e | ||
|
|
99cc47cf00 | ||
|
|
908b8b4352 | ||
|
|
1a7f475220 | ||
|
|
cb63d17a2f | ||
|
|
c8ce4631e2 | ||
|
|
96c2b0a2f7 | ||
|
|
23475c7a1b | ||
|
|
9ab3d3195e | ||
|
|
a22d07ae60 | ||
|
|
221658f6bf | ||
|
|
133df2e4be | ||
|
|
5fba9595af | ||
|
|
90ca77a46a | ||
|
|
f0c76c1349 | ||
|
|
6e5cfa9bf2 | ||
|
|
9ffcf2eca5 | ||
|
|
8514fb9bdc | ||
|
|
c1be7460c0 | ||
|
|
602aa4f948 | ||
|
|
2d2c55ce0e | ||
|
|
f0e207dff2 | ||
|
|
756e4356a5 | ||
|
|
9c303e8c23 | ||
|
|
a7ba4a0be8 | ||
|
|
54c2d7efe5 | ||
|
|
3f8eb3a2cd | ||
|
|
e095192968 | ||
|
|
862c9591a1 | ||
|
|
e6980a832b | ||
|
|
be49f70b09 | ||
|
|
7865574bd4 | ||
|
|
e513f05c09 |
@@ -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[];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ class BitcoinRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/summary', this.getStrippedBlockTransaction)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
||||
@@ -321,6 +322,20 @@ class BitcoinRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async getStrippedBlockTransaction(req: Request, res: Response) {
|
||||
try {
|
||||
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
|
||||
if (!transaction) {
|
||||
handleError(req, res, 404, `transaction not found in summary`);
|
||||
return;
|
||||
}
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||
res.json(transaction);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
}
|
||||
}
|
||||
|
||||
private async getBlock(req: Request, res: Response) {
|
||||
try {
|
||||
const block = await blocks.$getBlock(req.params.hash);
|
||||
|
||||
@@ -179,4 +179,11 @@ export namespace IEsploraApi {
|
||||
burn_count: number;
|
||||
}
|
||||
|
||||
export interface AddressTxSummary {
|
||||
txid: string;
|
||||
value: number;
|
||||
height: number;
|
||||
time: number;
|
||||
tx_position?: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -412,8 +412,16 @@ class Blocks {
|
||||
}
|
||||
|
||||
try {
|
||||
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||
const currentBlockHeight = blockchainInfo.blocks;
|
||||
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight);
|
||||
if (indexingBlockAmount <= -1) {
|
||||
indexingBlockAmount = currentBlockHeight + 1;
|
||||
}
|
||||
const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
|
||||
|
||||
// Get all indexed block hash
|
||||
const indexedBlocks = await blocksRepository.$getIndexedBlocks();
|
||||
const indexedBlocks = (await blocksRepository.$getIndexedBlocks()).filter(block => block.height >= lastBlockToIndex);
|
||||
const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
|
||||
|
||||
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
|
||||
@@ -1216,6 +1224,11 @@ class Blocks {
|
||||
return summary.transactions;
|
||||
}
|
||||
|
||||
public async $getSingleTxFromSummary(hash: string, txid: string): Promise<TransactionClassified | null> {
|
||||
const txs = await this.$getStrippedBlockTransactions(hash);
|
||||
return txs.find(tx => tx.txid === txid) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 15 blocks
|
||||
*
|
||||
|
||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 83;
|
||||
private static currentVersion = 93;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
private uniqueLogs: string[] = [];
|
||||
@@ -710,6 +710,97 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
|
||||
await this.updateToSchemaVersion(83);
|
||||
}
|
||||
|
||||
// add new pools indexes
|
||||
if (databaseSchemaVersion < 84 && isBitcoin === true) {
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`pools\`
|
||||
ADD INDEX \`slug\` (\`slug\`),
|
||||
ADD INDEX \`unique_id\` (\`unique_id\`)
|
||||
`);
|
||||
await this.updateToSchemaVersion(84);
|
||||
}
|
||||
|
||||
// lightning channels indexes
|
||||
if (databaseSchemaVersion < 85 && isBitcoin === true) {
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`channels\`
|
||||
ADD INDEX \`created\` (\`created\`),
|
||||
ADD INDEX \`capacity\` (\`capacity\`),
|
||||
ADD INDEX \`closing_reason\` (\`closing_reason\`),
|
||||
ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
|
||||
`);
|
||||
await this.updateToSchemaVersion(85);
|
||||
}
|
||||
|
||||
// lightning nodes indexes
|
||||
if (databaseSchemaVersion < 86 && isBitcoin === true) {
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`nodes\`
|
||||
ADD INDEX \`status\` (\`status\`),
|
||||
ADD INDEX \`channels\` (\`channels\`),
|
||||
ADD INDEX \`country_id\` (\`country_id\`),
|
||||
ADD INDEX \`as_number\` (\`as_number\`),
|
||||
ADD INDEX \`first_seen\` (\`first_seen\`)
|
||||
`);
|
||||
await this.updateToSchemaVersion(86);
|
||||
}
|
||||
|
||||
// lightning node sockets indexes
|
||||
if (databaseSchemaVersion < 87 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
|
||||
await this.updateToSchemaVersion(87);
|
||||
}
|
||||
|
||||
// lightning stats indexes
|
||||
if (databaseSchemaVersion < 88 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
|
||||
await this.updateToSchemaVersion(88);
|
||||
}
|
||||
|
||||
// geo names indexes
|
||||
if (databaseSchemaVersion < 89 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
|
||||
await this.updateToSchemaVersion(89);
|
||||
}
|
||||
|
||||
// hashrates indexes
|
||||
if (databaseSchemaVersion < 90 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
|
||||
await this.updateToSchemaVersion(90);
|
||||
}
|
||||
|
||||
// block audits indexes
|
||||
if (databaseSchemaVersion < 91 && isBitcoin === true) {
|
||||
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
|
||||
await this.updateToSchemaVersion(91);
|
||||
}
|
||||
|
||||
// elements_pegs indexes
|
||||
if (databaseSchemaVersion < 92 && config.MEMPOOL.NETWORK === 'liquid') {
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`elements_pegs\`
|
||||
ADD INDEX \`block\` (\`block\`),
|
||||
ADD INDEX \`datetime\` (\`datetime\`),
|
||||
ADD INDEX \`amount\` (\`amount\`),
|
||||
ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
|
||||
ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
|
||||
`);
|
||||
await this.updateToSchemaVersion(92);
|
||||
}
|
||||
|
||||
// federation_txos indexes
|
||||
if (databaseSchemaVersion < 93 && config.MEMPOOL.NETWORK === 'liquid') {
|
||||
await this.$executeQuery(`
|
||||
ALTER TABLE \`federation_txos\`
|
||||
ADD INDEX \`unspent\` (\`unspent\`),
|
||||
ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
|
||||
ADD INDEX \`blocktime\` (\`blocktime\`),
|
||||
ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
|
||||
ADD INDEX \`expiredAt\` (\`expiredAt\`)
|
||||
`);
|
||||
await this.updateToSchemaVersion(93);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -382,7 +382,7 @@ class MempoolBlocks {
|
||||
|
||||
const ancestors: Ancestor[] = [];
|
||||
const descendants: Ancestor[] = [];
|
||||
let ancestor: MempoolTransactionExtended
|
||||
let ancestor: MempoolTransactionExtended;
|
||||
for (const cluster of clusters) {
|
||||
for (const memberTxid of cluster) {
|
||||
const mempoolTx = mempool[memberTxid];
|
||||
@@ -462,7 +462,7 @@ class MempoolBlocks {
|
||||
|
||||
for (let i = 0; i < block.length; i++) {
|
||||
const txid = block[i];
|
||||
if (txid) {
|
||||
if (txid in mempool) {
|
||||
mempoolTx = mempool[txid];
|
||||
// save position in projected blocks
|
||||
mempoolTx.position = {
|
||||
@@ -481,6 +481,9 @@ class MempoolBlocks {
|
||||
mempoolTx.acceleratedAt = acceleration?.added;
|
||||
mempoolTx.feeDelta = acceleration?.feeDelta;
|
||||
for (const ancestor of mempoolTx.ancestors || []) {
|
||||
if (!(ancestor.txid in mempool)) {
|
||||
continue;
|
||||
}
|
||||
if (!mempool[ancestor.txid].acceleration) {
|
||||
mempool[ancestor.txid].cpfpDirty = true;
|
||||
}
|
||||
@@ -688,7 +691,7 @@ class MempoolBlocks {
|
||||
[pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean };
|
||||
} = {};
|
||||
// prepare a list of accelerations in ascending order (we'll pop items off the end of the list)
|
||||
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).map(acc => {
|
||||
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).filter(acc => acc.txid in mempoolCache).map(acc => {
|
||||
let vsize = mempoolCache[acc.txid].vsize;
|
||||
for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
|
||||
vsize += (ancestor.weight / 4);
|
||||
|
||||
@@ -10,6 +10,7 @@ import bitcoinClient from './bitcoin/bitcoin-client';
|
||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import rbfCache from './rbf-cache';
|
||||
import { Acceleration } from './services/acceleration';
|
||||
import accelerationApi from './services/acceleration';
|
||||
import redisCache from './redis-cache';
|
||||
import blocks from './blocks';
|
||||
|
||||
@@ -207,7 +208,7 @@ class Mempool {
|
||||
return txTimes;
|
||||
}
|
||||
|
||||
public async $updateMempool(transactions: string[], accelerations: Acceleration[] | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
|
||||
public async $updateMempool(transactions: string[], accelerations: Record<string, Acceleration> | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
|
||||
logger.debug(`Updating mempool...`);
|
||||
|
||||
// warn if this run stalls the main loop for more than 2 minutes
|
||||
@@ -354,7 +355,7 @@ class Mempool {
|
||||
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
||||
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
|
||||
|
||||
const accelerationDelta = accelerations != null ? await this.$updateAccelerations(accelerations) : [];
|
||||
const accelerationDelta = accelerations != null ? await this.updateAccelerations(accelerations) : [];
|
||||
if (accelerationDelta.length) {
|
||||
hasChange = true;
|
||||
}
|
||||
@@ -399,58 +400,11 @@ class Mempool {
|
||||
return this.accelerations;
|
||||
}
|
||||
|
||||
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
|
||||
public updateAccelerations(newAccelerationMap: Record<string, Acceleration>): string[] {
|
||||
try {
|
||||
const changed: string[] = [];
|
||||
|
||||
const newAccelerationMap: { [txid: string]: Acceleration } = {};
|
||||
for (const acceleration of newAccelerations) {
|
||||
// skip transactions we don't know about
|
||||
if (!this.mempoolCache[acceleration.txid]) {
|
||||
continue;
|
||||
}
|
||||
newAccelerationMap[acceleration.txid] = acceleration;
|
||||
if (this.accelerations[acceleration.txid] == null) {
|
||||
// new acceleration
|
||||
changed.push(acceleration.txid);
|
||||
} else {
|
||||
if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) {
|
||||
// feeDelta changed
|
||||
changed.push(acceleration.txid);
|
||||
} else if (this.accelerations[acceleration.txid].pools?.length) {
|
||||
let poolsChanged = false;
|
||||
const pools = new Set();
|
||||
this.accelerations[acceleration.txid].pools.forEach(pool => {
|
||||
pools.add(pool);
|
||||
});
|
||||
acceleration.pools.forEach(pool => {
|
||||
if (!pools.has(pool)) {
|
||||
poolsChanged = true;
|
||||
} else {
|
||||
pools.delete(pool);
|
||||
}
|
||||
});
|
||||
if (pools.size > 0) {
|
||||
poolsChanged = true;
|
||||
}
|
||||
if (poolsChanged) {
|
||||
// pools changed
|
||||
changed.push(acceleration.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const oldTxid of Object.keys(this.accelerations)) {
|
||||
if (!newAccelerationMap[oldTxid]) {
|
||||
// removed
|
||||
changed.push(oldTxid);
|
||||
}
|
||||
}
|
||||
|
||||
const accelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, newAccelerationMap);
|
||||
this.accelerations = newAccelerationMap;
|
||||
|
||||
return changed;
|
||||
return accelerationDelta;
|
||||
} catch (e: any) {
|
||||
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
|
||||
return [];
|
||||
|
||||
@@ -459,7 +459,7 @@ class MiningRoutes {
|
||||
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||
return;
|
||||
}
|
||||
res.status(200).send(accelerationApi.accelerations || []);
|
||||
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
|
||||
} catch (e) {
|
||||
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||
}
|
||||
|
||||
@@ -136,9 +136,13 @@ class Mining {
|
||||
poolsStatistics['blockCount'] = blockCount;
|
||||
|
||||
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h');
|
||||
const totalBlock3d: number = await BlocksRepository.$blockCount(null, '3d');
|
||||
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
|
||||
|
||||
try {
|
||||
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
|
||||
poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d);
|
||||
poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w);
|
||||
} catch (e) {
|
||||
poolsStatistics['lastEstimatedHashrate'] = 0;
|
||||
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { WebSocket } from 'ws';
|
||||
import config from '../../config';
|
||||
import logger from '../../logger';
|
||||
import { BlockExtended } from '../../mempool.interfaces';
|
||||
import axios from 'axios';
|
||||
import mempool from '../mempool';
|
||||
import websocketHandler from '../websocket-handler';
|
||||
|
||||
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
|
||||
|
||||
@@ -37,14 +40,23 @@ export interface AccelerationHistory {
|
||||
};
|
||||
|
||||
class AccelerationApi {
|
||||
private ws: WebSocket | null = null;
|
||||
private useWebsocket: boolean = config.MEMPOOL.OFFICIAL && config.MEMPOOL_SERVICES.ACCELERATIONS;
|
||||
private startedWebsocketLoop: boolean = false;
|
||||
private websocketConnected: boolean = false;
|
||||
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
|
||||
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
|
||||
private _accelerations: Acceleration[] | null = null;
|
||||
private websocketPath = config.MEMPOOL_SERVICES?.API ? `${config.MEMPOOL_SERVICES.API.replace('https://', 'wss://').replace('http://', 'ws://')}/accelerator/ws` : '/';
|
||||
private _accelerations: Record<string, Acceleration> = {};
|
||||
private lastPoll = 0;
|
||||
private lastPing = Date.now();
|
||||
private lastPong = Date.now();
|
||||
private forcePoll = false;
|
||||
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
|
||||
|
||||
public get accelerations(): Acceleration[] | null {
|
||||
public constructor() {}
|
||||
|
||||
public getAccelerations(): Record<string, Acceleration> {
|
||||
return this._accelerations;
|
||||
}
|
||||
|
||||
@@ -72,11 +84,18 @@ class AccelerationApi {
|
||||
}
|
||||
}
|
||||
|
||||
public async $updateAccelerations(): Promise<Acceleration[] | null> {
|
||||
public async $updateAccelerations(): Promise<Record<string, Acceleration> | null> {
|
||||
if (this.useWebsocket && this.websocketConnected) {
|
||||
return this._accelerations;
|
||||
}
|
||||
if (!this.onDemandPollingEnabled) {
|
||||
const accelerations = await this.$fetchAccelerations();
|
||||
if (accelerations) {
|
||||
this._accelerations = accelerations;
|
||||
const latestAccelerations = {};
|
||||
for (const acc of accelerations) {
|
||||
latestAccelerations[acc.txid] = acc;
|
||||
}
|
||||
this._accelerations = latestAccelerations;
|
||||
return this._accelerations;
|
||||
}
|
||||
} else {
|
||||
@@ -85,7 +104,7 @@ class AccelerationApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
|
||||
private async $updateAccelerationsOnDemand(): Promise<Record<string, Acceleration> | null> {
|
||||
const shouldUpdate = this.forcePoll
|
||||
|| this.countMyAccelerationsWithStatus('requested') > 0
|
||||
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
|
||||
@@ -120,7 +139,11 @@ class AccelerationApi {
|
||||
}
|
||||
}
|
||||
|
||||
this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[];
|
||||
const latestAccelerations = {};
|
||||
for (const acc of Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]) {
|
||||
latestAccelerations[acc.txid] = acc;
|
||||
}
|
||||
this._accelerations = latestAccelerations;
|
||||
return this._accelerations;
|
||||
}
|
||||
|
||||
@@ -152,6 +175,148 @@ class AccelerationApi {
|
||||
}
|
||||
return anyAccelerated;
|
||||
}
|
||||
|
||||
// get a list of accelerations that have changed between two sets of accelerations
|
||||
public getAccelerationDelta(oldAccelerationMap: Record<string, Acceleration>, newAccelerationMap: Record<string, Acceleration>): string[] {
|
||||
const changed: string[] = [];
|
||||
const mempoolCache = mempool.getMempool();
|
||||
|
||||
for (const acceleration of Object.values(newAccelerationMap)) {
|
||||
// skip transactions we don't know about
|
||||
if (!mempoolCache[acceleration.txid]) {
|
||||
continue;
|
||||
}
|
||||
if (oldAccelerationMap[acceleration.txid] == null) {
|
||||
// new acceleration
|
||||
changed.push(acceleration.txid);
|
||||
} else {
|
||||
if (oldAccelerationMap[acceleration.txid].feeDelta !== acceleration.feeDelta) {
|
||||
// feeDelta changed
|
||||
changed.push(acceleration.txid);
|
||||
} else if (oldAccelerationMap[acceleration.txid].pools?.length) {
|
||||
let poolsChanged = false;
|
||||
const pools = new Set();
|
||||
oldAccelerationMap[acceleration.txid].pools.forEach(pool => {
|
||||
pools.add(pool);
|
||||
});
|
||||
acceleration.pools.forEach(pool => {
|
||||
if (!pools.has(pool)) {
|
||||
poolsChanged = true;
|
||||
} else {
|
||||
pools.delete(pool);
|
||||
}
|
||||
});
|
||||
if (pools.size > 0) {
|
||||
poolsChanged = true;
|
||||
}
|
||||
if (poolsChanged) {
|
||||
// pools changed
|
||||
changed.push(acceleration.txid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const oldTxid of Object.keys(oldAccelerationMap)) {
|
||||
if (!newAccelerationMap[oldTxid]) {
|
||||
// removed
|
||||
changed.push(oldTxid);
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
private handleWebsocketMessage(msg: any): void {
|
||||
if (msg?.accelerations !== null) {
|
||||
const latestAccelerations = {};
|
||||
for (const acc of msg?.accelerations || []) {
|
||||
latestAccelerations[acc.txid] = acc;
|
||||
}
|
||||
this._accelerations = latestAccelerations;
|
||||
websocketHandler.handleAccelerationsChanged(this._accelerations);
|
||||
}
|
||||
}
|
||||
|
||||
public async connectWebsocket(): Promise<void> {
|
||||
if (this.startedWebsocketLoop) {
|
||||
return;
|
||||
}
|
||||
while (this.useWebsocket) {
|
||||
this.startedWebsocketLoop = true;
|
||||
if (!this.ws) {
|
||||
this.ws = new WebSocket(this.websocketPath);
|
||||
this.lastPing = 0;
|
||||
|
||||
this.ws.on('open', () => {
|
||||
logger.info(`Acceleration websocket opened to ${this.websocketPath}`);
|
||||
this.websocketConnected = true;
|
||||
this.ws?.send(JSON.stringify({
|
||||
'watch-accelerations': true
|
||||
}));
|
||||
});
|
||||
|
||||
this.ws.on('error', (error) => {
|
||||
let errMsg = `Acceleration websocket error on ${this.websocketPath}: ${error['code']}`;
|
||||
if (error['errors']) {
|
||||
errMsg += ' - ' + error['errors'].join(' - ');
|
||||
}
|
||||
logger.err(errMsg);
|
||||
this.ws = null;
|
||||
this.websocketConnected = false;
|
||||
});
|
||||
|
||||
this.ws.on('close', () => {
|
||||
logger.info('Acceleration websocket closed');
|
||||
this.ws = null;
|
||||
this.websocketConnected = false;
|
||||
});
|
||||
|
||||
this.ws.on('message', (data, isBinary) => {
|
||||
try {
|
||||
const msg = (isBinary ? data : data.toString()) as string;
|
||||
const parsedMsg = msg?.length ? JSON.parse(msg) : null;
|
||||
this.handleWebsocketMessage(parsedMsg);
|
||||
} catch (e) {
|
||||
logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on('ping', () => {
|
||||
logger.debug('received ping from acceleration websocket server');
|
||||
});
|
||||
|
||||
this.ws.on('pong', () => {
|
||||
logger.debug('received pong from acceleration websocket server');
|
||||
this.lastPong = Date.now();
|
||||
});
|
||||
} else if (this.websocketConnected) {
|
||||
if (this.lastPing && this.lastPing > this.lastPong && (Date.now() - this.lastPing > 10000)) {
|
||||
logger.warn('No pong received within 10 seconds, terminating connection');
|
||||
try {
|
||||
this.ws?.terminate();
|
||||
} catch (e) {
|
||||
logger.warn('failed to terminate acceleration websocket connection: ' + (e instanceof Error ? e.message : e));
|
||||
} finally {
|
||||
this.ws = null;
|
||||
this.websocketConnected = false;
|
||||
this.lastPing = 0;
|
||||
}
|
||||
} else if (!this.lastPing || (Date.now() - this.lastPing > 30000)) {
|
||||
logger.debug('sending ping to acceleration websocket server');
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
this.ws?.ping();
|
||||
this.lastPing = Date.now();
|
||||
} catch (e) {
|
||||
logger.warn('failed to send ping to acceleration websocket server: ' + (e instanceof Error ? e.message : e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccelerationApi();
|
||||
26
backend/src/api/services/services-routes.ts
Normal file
26
backend/src/api/services/services-routes.ts
Normal 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();
|
||||
153
backend/src/api/services/wallets.ts
Normal file
153
backend/src/api/services/wallets.ts
Normal 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();
|
||||
@@ -22,11 +22,13 @@ import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository
|
||||
import Audit from './audit';
|
||||
import priceUpdater from '../tasks/price-updater';
|
||||
import { ApiPrice } from '../repositories/PricesRepository';
|
||||
import { Acceleration } from './services/acceleration';
|
||||
import accelerationApi from './services/acceleration';
|
||||
import mempool from './mempool';
|
||||
import statistics from './statistics/statistics';
|
||||
import accelerationRepository from '../repositories/AccelerationRepository';
|
||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||
import walletApi from './services/wallets';
|
||||
|
||||
interface AddressTransactions {
|
||||
mempool: MempoolTransactionExtended[],
|
||||
@@ -59,6 +61,8 @@ class WebsocketHandler {
|
||||
private lastRbfSummary: ReplacementInfo[] | null = null;
|
||||
private mempoolSequence: number = 0;
|
||||
|
||||
private accelerations: Record<string, Acceleration> = {};
|
||||
|
||||
constructor() { }
|
||||
|
||||
addWebsocketServer(wss: WebSocket.Server) {
|
||||
@@ -307,6 +311,14 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-wallet']) {
|
||||
if (parsedMessage['track-wallet'] === 'stop') {
|
||||
client['track-wallet'] = null;
|
||||
} else {
|
||||
client['track-wallet'] = parsedMessage['track-wallet'];
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedMessage && parsedMessage['track-asset']) {
|
||||
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) {
|
||||
client['track-asset'] = parsedMessage['track-asset'];
|
||||
@@ -486,6 +498,42 @@ class WebsocketHandler {
|
||||
}
|
||||
}
|
||||
|
||||
handleAccelerationsChanged(accelerations: Record<string, Acceleration>): void {
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server has been set');
|
||||
}
|
||||
|
||||
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
|
||||
this.accelerations = accelerations;
|
||||
|
||||
if (!websocketAccelerationDelta.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// pre-compute acceleration delta
|
||||
const accelerationUpdate = {
|
||||
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
|
||||
};
|
||||
|
||||
try {
|
||||
const response = JSON.stringify({
|
||||
accelerations: accelerationUpdate,
|
||||
});
|
||||
|
||||
for (const server of this.webSocketServers) {
|
||||
server.clients.forEach((client) => {
|
||||
if (client.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
client.send(response);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug(`Error sending acceleration update to websocket clients: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleReorg(): void {
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
@@ -562,7 +610,7 @@ class WebsocketHandler {
|
||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
const accelerations = memPool.getAccelerations();
|
||||
const accelerations = accelerationApi.getAccelerations();
|
||||
memPool.handleRbfTransactions(rbfTransactions);
|
||||
const rbfChanges = rbfCache.getRbfChanges();
|
||||
let rbfReplacements;
|
||||
@@ -670,10 +718,13 @@ class WebsocketHandler {
|
||||
const addressCache = this.makeAddressCache(newTransactions);
|
||||
const removedAddressCache = this.makeAddressCache(deletedTransactions);
|
||||
|
||||
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
|
||||
this.accelerations = accelerations;
|
||||
|
||||
// pre-compute acceleration delta
|
||||
const accelerationUpdate = {
|
||||
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||
removed: accelerationDelta.filter(txid => !accelerations[txid]),
|
||||
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
|
||||
};
|
||||
|
||||
// TODO - Fix indentation after PR is merged
|
||||
@@ -1112,6 +1163,9 @@ class WebsocketHandler {
|
||||
replaced: replacedTransactions,
|
||||
};
|
||||
|
||||
// check for wallet transactions
|
||||
const walletTransactions = config.WALLETS.ENABLED ? walletApi.processBlock(block, transactions) : [];
|
||||
|
||||
const responseCache = { ...this.socketData };
|
||||
function getCachedResponse(key, data): string {
|
||||
if (!responseCache[key]) {
|
||||
@@ -1316,6 +1370,11 @@ class WebsocketHandler {
|
||||
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
|
||||
}
|
||||
|
||||
if (client['track-wallet']) {
|
||||
const trackedWallet = client['track-wallet'];
|
||||
response['wallet-transactions'] = getCachedResponse(`wallet-transactions-${trackedWallet}`, walletTransactions[trackedWallet] ?? {});
|
||||
}
|
||||
|
||||
if (Object.keys(response).length) {
|
||||
client.send(this.serializeResponse(response));
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -32,6 +32,7 @@ import pricesRoutes from './api/prices/prices.routes';
|
||||
import miningRoutes from './api/mining/mining-routes';
|
||||
import liquidRoutes from './api/liquid/liquid.routes';
|
||||
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
|
||||
import servicesRoutes from './api/services/services-routes';
|
||||
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
|
||||
import forensicsService from './tasks/lightning/forensics.service';
|
||||
import priceUpdater from './tasks/price-updater';
|
||||
@@ -46,6 +47,7 @@ import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
|
||||
import accelerationRoutes from './api/acceleration/acceleration.routes';
|
||||
import aboutRoutes from './api/about.routes';
|
||||
import mempoolBlocks from './api/mempool-blocks';
|
||||
import walletApi from './api/services/wallets';
|
||||
|
||||
class Server {
|
||||
private wss: WebSocket.Server | undefined;
|
||||
@@ -231,13 +233,17 @@ class Server {
|
||||
const newMempool = await bitcoinApi.$getRawMempool();
|
||||
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
|
||||
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
|
||||
const newAccelerations = await accelerationApi.$updateAccelerations();
|
||||
const latestAccelerations = await accelerationApi.$updateAccelerations();
|
||||
const numHandledBlocks = await blocks.$updateBlocks();
|
||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
||||
if (numHandledBlocks === 0) {
|
||||
await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate);
|
||||
await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate);
|
||||
}
|
||||
indexer.$run();
|
||||
if (config.WALLETS.ENABLED) {
|
||||
// might take a while, so run in the background
|
||||
walletApi.$syncWallets();
|
||||
}
|
||||
if (config.FIAT_PRICE.ENABLED) {
|
||||
priceUpdater.$run();
|
||||
}
|
||||
@@ -312,8 +318,10 @@ class Server {
|
||||
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
||||
}
|
||||
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
||||
|
||||
accelerationApi.connectWebsocket();
|
||||
}
|
||||
|
||||
|
||||
setUpHttpApiRoutes(): void {
|
||||
bitcoinRoutes.initRoutes(this.app);
|
||||
bitcoinCoreRoutes.initRoutes(this.app);
|
||||
@@ -335,6 +343,9 @@ class Server {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
accelerationRoutes.initRoutes(this.app);
|
||||
}
|
||||
if (config.WALLETS.ENABLED) {
|
||||
servicesRoutes.initRoutes(this.app);
|
||||
}
|
||||
if (!config.MEMPOOL.OFFICIAL) {
|
||||
aboutRoutes.initRoutes(this.app);
|
||||
}
|
||||
|
||||
@@ -501,7 +501,7 @@ class BlocksRepository {
|
||||
}
|
||||
|
||||
query += ` ORDER BY height DESC
|
||||
LIMIT 10`;
|
||||
LIMIT 100`;
|
||||
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(query, params);
|
||||
|
||||
48
frontend/custom-bitb-config.json
Normal file
48
frontend/custom-bitb-config.json
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
51
frontend/custom-meta-config.json
Normal file
51
frontend/custom-meta-config.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"theme": "contrast",
|
||||
"enterprise": "meta",
|
||||
"branding": {
|
||||
"name": "metaplanet",
|
||||
"title": "Metaplanet",
|
||||
"site_id": 21,
|
||||
"header_img": "/resources/metalogo.svg",
|
||||
"footer_img": "/resources/metalogo.svg"
|
||||
},
|
||||
"dashboard": {
|
||||
"widgets": [
|
||||
{
|
||||
"component": "fees",
|
||||
"mobileOrder": 4
|
||||
},
|
||||
{
|
||||
"component": "walletBalance",
|
||||
"mobileOrder": 1,
|
||||
"props": {
|
||||
"wallet": "3350"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "twitter",
|
||||
"mobileOrder": 5,
|
||||
"props": {
|
||||
"handle": "Metaplanet_JP"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "wallet",
|
||||
"mobileOrder": 2,
|
||||
"props": {
|
||||
"wallet": "3350",
|
||||
"period": "all"
|
||||
}
|
||||
},
|
||||
{
|
||||
"component": "blocks"
|
||||
},
|
||||
{
|
||||
"component": "walletTransactions",
|
||||
"mobileOrder": 3,
|
||||
"props": {
|
||||
"wallet": "3350"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AppPreloadingStrategy } from './app.preloading-strategy'
|
||||
import { BlockViewComponent } from './components/block-view/block-view.component';
|
||||
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
|
||||
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
|
||||
import { ClockComponent } from './components/clock/clock.component';
|
||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||
import { AddressGroupComponent } from './components/address-group/address-group.component';
|
||||
import { TrackerComponent } from './components/tracker/tracker.component';
|
||||
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component';
|
||||
import { TrackerGuard } from './route-guards';
|
||||
import { AppPreloadingStrategy } from '@app/app.preloading-strategy'
|
||||
import { BlockViewComponent } from '@components/block-view/block-view.component';
|
||||
import { EightBlocksComponent } from '@components/eight-blocks/eight-blocks.component';
|
||||
import { MempoolBlockViewComponent } from '@components/mempool-block-view/mempool-block-view.component';
|
||||
import { ClockComponent } from '@components/clock/clock.component';
|
||||
import { StatusViewComponent } from '@components/status-view/status-view.component';
|
||||
import { AddressGroupComponent } from '@components/address-group/address-group.component';
|
||||
import { TrackerComponent } from '@components/tracker/tracker.component';
|
||||
import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component';
|
||||
import { TrackerGuard } from '@app/route-guards';
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
@@ -22,16 +22,16 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
path: 'widget/wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
@@ -45,7 +45,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
@@ -60,12 +60,12 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
@@ -83,7 +83,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
@@ -103,16 +103,16 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
path: 'widget/wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
@@ -126,7 +126,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
@@ -138,22 +138,22 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
canMatch: [TrackerGuard],
|
||||
runGuardsAndResolvers: 'always',
|
||||
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
|
||||
loadChildren: () => import('@components/tracker/tracker.module').then(m => m.TrackerModule),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
path: 'widget/wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
@@ -165,19 +165,19 @@ let routes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'testnet',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'testnet4',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'signet',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -212,7 +212,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
];
|
||||
@@ -225,16 +225,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
path: 'widget/wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
@@ -248,7 +248,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
@@ -260,16 +260,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
path: 'widget/wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
@@ -281,11 +281,11 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'testnet',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -296,7 +296,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,11 +2,11 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ServerModule } from '@angular/platform-server';
|
||||
|
||||
import { ZONE_SERVICE } from './injection-tokens';
|
||||
import { ZONE_SERVICE } from '@app/injection-tokens';
|
||||
import { AppModule } from './app.module';
|
||||
import { AppComponent } from './components/app/app.component';
|
||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
import { ZoneService } from './services/zone.service';
|
||||
import { AppComponent } from '@components/app/app.component';
|
||||
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
|
||||
import { ZoneService } from '@app/services/zone.service';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@@ -20,4 +20,4 @@ import { ZoneService } from './services/zone.service';
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppServerModule {}
|
||||
export class AppServerModule {}
|
||||
|
||||
@@ -2,33 +2,33 @@ import { BrowserModule } from '@angular/platform-browser';
|
||||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ZONE_SERVICE } from './injection-tokens';
|
||||
import { ZONE_SERVICE } from '@app/injection-tokens';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './components/app/app.component';
|
||||
import { ElectrsApiService } from './services/electrs-api.service';
|
||||
import { OrdApiService } from './services/ord-api.service';
|
||||
import { StateService } from './services/state.service';
|
||||
import { CacheService } from './services/cache.service';
|
||||
import { PriceService } from './services/price.service';
|
||||
import { EnterpriseService } from './services/enterprise.service';
|
||||
import { WebsocketService } from './services/websocket.service';
|
||||
import { AudioService } from './services/audio.service';
|
||||
import { PreloadService } from './services/preload.service';
|
||||
import { SeoService } from './services/seo.service';
|
||||
import { OpenGraphService } from './services/opengraph.service';
|
||||
import { ZoneService } from './services/zone-shim.service';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { StorageService } from './services/storage.service';
|
||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
import { LanguageService } from './services/language.service';
|
||||
import { ThemeService } from './services/theme.service';
|
||||
import { TimeService } from './services/time.service';
|
||||
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
|
||||
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
|
||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
|
||||
import { AppPreloadingStrategy } from './app.preloading-strategy';
|
||||
import { ServicesApiServices } from './services/services-api.service';
|
||||
import { AppComponent } from '@components/app/app.component';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
import { OrdApiService } from '@app/services/ord-api.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { CacheService } from '@app/services/cache.service';
|
||||
import { PriceService } from '@app/services/price.service';
|
||||
import { EnterpriseService } from '@app/services/enterprise.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { PreloadService } from '@app/services/preload.service';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||
import { ZoneService } from '@app/services/zone-shim.service';
|
||||
import { SharedModule } from '@app/shared/shared.module';
|
||||
import { StorageService } from '@app/services/storage.service';
|
||||
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
|
||||
import { LanguageService } from '@app/services/language.service';
|
||||
import { ThemeService } from '@app/services/theme.service';
|
||||
import { TimeService } from '@app/services/time.service';
|
||||
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
|
||||
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
|
||||
import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||
import { CapAddressPipe } from '@app/shared/pipes/cap-address-pipe/cap-address-pipe';
|
||||
import { AppPreloadingStrategy } from '@app/app.preloading-strategy';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { DatePipe } from '@angular/common';
|
||||
|
||||
const providers = [
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -172,10 +172,6 @@
|
||||
background-color: var(--tertiary);
|
||||
}
|
||||
|
||||
.btn-small-height {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/* eslint-disable no-console */
|
||||
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
|
||||
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
import { md5, insecureRandomUUID } from '../../shared/common.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
import { ETA, EtaService } from '../../services/eta.service';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { MiningStats } from '../../services/mining.service';
|
||||
import { IAuth, AuthServiceMempool } from '../../services/auth.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { md5 } from '@app/shared/common.utils';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { ETA, EtaService } from '@app/services/eta.service';
|
||||
import { Transaction } from '@interfaces/electrs.interface';
|
||||
import { MiningStats } from '@app/services/mining.service';
|
||||
import { IAuth, AuthServiceMempool } from '@app/services/auth.service';
|
||||
import { EnterpriseService } from '@app/services/enterprise.service';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { isDevMode } from '@angular/core';
|
||||
|
||||
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
|
||||
@@ -84,13 +84,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
timePaid: number = 0; // time acceleration requested
|
||||
math = Math;
|
||||
isMobile: boolean = window.innerWidth <= 767.98;
|
||||
isProdDomain = ['mempool.space',
|
||||
'mempool-staging.va1.mempool.space',
|
||||
'mempool-staging.fmt.mempool.space',
|
||||
'mempool-staging.fra.mempool.space',
|
||||
'mempool-staging.tk7.mempool.space',
|
||||
'mempool-staging.sg1.mempool.space'
|
||||
].indexOf(document.location.hostname) > -1;
|
||||
isProdDomain = false;
|
||||
|
||||
private _step: CheckoutStep = 'summary';
|
||||
simpleMode: boolean = true;
|
||||
@@ -100,7 +94,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
auth: IAuth | null = null;
|
||||
|
||||
// accelerator stuff
|
||||
accelerationUUID: string;
|
||||
accelerationSubscription: Subscription;
|
||||
difficultySubscription: Subscription;
|
||||
estimateSubscription: Subscription;
|
||||
@@ -143,7 +136,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
private authService: AuthServiceMempool,
|
||||
private enterpriseService: EnterpriseService,
|
||||
) {
|
||||
this.accelerationUUID = insecureRandomUUID();
|
||||
this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1;
|
||||
|
||||
// Check if Apple Pay available
|
||||
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
|
||||
@@ -207,6 +200,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
moveToStep(step: CheckoutStep): void {
|
||||
this.processing = false;
|
||||
this._step = step;
|
||||
if (this.timeoutTimer) {
|
||||
clearTimeout(this.timeoutTimer);
|
||||
@@ -392,7 +386,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.accelerationSubscription = this.servicesApiService.accelerate$(
|
||||
this.tx.txid,
|
||||
this.userBid,
|
||||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.processing = false;
|
||||
@@ -526,7 +519,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
tokenResult.token,
|
||||
cardTag,
|
||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
this.accelerationUUID,
|
||||
costUSD
|
||||
).subscribe({
|
||||
next: () => {
|
||||
@@ -626,7 +618,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
tokenResult.token,
|
||||
cardTag,
|
||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
this.accelerationUUID,
|
||||
costUSD
|
||||
).subscribe({
|
||||
next: () => {
|
||||
@@ -717,7 +708,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
tokenResult.token,
|
||||
tokenResult.details.cashAppPay.cashtag,
|
||||
tokenResult.details.cashAppPay.referenceId,
|
||||
this.accelerationUUID,
|
||||
costUSD
|
||||
).subscribe({
|
||||
next: () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="acceleration-list">
|
||||
<div class="acceleration-list" *ngIf="{ accelerations: accelerationList$ | async } as state">
|
||||
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
|
||||
<thead>
|
||||
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
@@ -21,8 +21,8 @@
|
||||
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
|
||||
</ng-container>
|
||||
</thead>
|
||||
<tbody *ngIf="accelerationList$ | async as accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tr *ngFor="let acceleration of accelerations; let i= index;">
|
||||
<tbody *ngIf="state.accelerations && nonEmptyAccelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tr *ngFor="let acceleration of state.accelerations; let i= index;">
|
||||
<td class="txid text-left">
|
||||
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
|
||||
<app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
@if (hasCpfp) {
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
|
||||
}
|
||||
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
||||
</div>
|
||||
@@ -36,7 +36,7 @@
|
||||
<tr>
|
||||
<td colspan="3" class="pt-0">
|
||||
<div class="d-flex justify-content-end align-items-start">
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { echarts, EChartsOption } from '@app/graphs/echarts';
|
||||
import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
|
||||
import { catchError, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { AddressTxSummary, ChainStats } from '@interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
|
||||
import { Router } from '@angular/router';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { PriceService } from '../../services/price.service';
|
||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
||||
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { PriceService } from '@app/services/price.service';
|
||||
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
|
||||
|
||||
const periodSeconds = {
|
||||
'1d': (60 * 60 * 24),
|
||||
@@ -77,13 +76,12 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private priceService: PriceService,
|
||||
private fiatCurrencyPipe: FiatCurrencyPipe,
|
||||
private fiatShortenerPipe: FiatShortenerPipe,
|
||||
private zone: NgZone,
|
||||
) {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.isLoading = true;
|
||||
if (!this.address || !this.stats) {
|
||||
if (!this.addressSummary$ && (!this.address || !this.stats)) {
|
||||
return;
|
||||
}
|
||||
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
|
||||
@@ -144,15 +142,16 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
|
||||
prepareChartOptions(summary: AddressTxSummary[]) {
|
||||
if (!summary || !this.stats) {
|
||||
if (!summary) {
|
||||
return;
|
||||
}
|
||||
|
||||
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum);
|
||||
const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0);
|
||||
let runningTotal = total;
|
||||
const processData = summary.map(d => {
|
||||
const balance = total;
|
||||
const fiatBalance = total * d.price / 100_000_000;
|
||||
total -= d.value;
|
||||
const balance = runningTotal;
|
||||
const fiatBalance = runningTotal * d.price / 100_000_000;
|
||||
runningTotal -= d.value;
|
||||
return {
|
||||
time: d.time * 1000,
|
||||
balance,
|
||||
@@ -172,7 +171,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
this.fiatData = this.fiatData.filter(d => d[0] >= startFiat);
|
||||
}
|
||||
this.data.push(
|
||||
{value: [now, this.stats.funded_txo_sum - this.stats.spent_txo_sum], symbol: 'none', tooltip: { show: false }}
|
||||
{value: [now, total], symbol: 'none', tooltip: { show: false }}
|
||||
);
|
||||
|
||||
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);
|
||||
@@ -244,18 +243,19 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
let tooltip = '<div>';
|
||||
|
||||
const hasTx = data[0].data[2].txid;
|
||||
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
|
||||
tooltip += `<div>
|
||||
<div style="text-align: right;">
|
||||
<div><b>${date}</b></div>`;
|
||||
|
||||
if (hasTx) {
|
||||
const header = data.length === 1
|
||||
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
|
||||
: `${data.length} transactions`;
|
||||
tooltip += `<span><b>${header}</b></span>`;
|
||||
tooltip += `<div><b>${header}</b></div>`;
|
||||
}
|
||||
|
||||
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
|
||||
|
||||
tooltip += `<div>
|
||||
<div style="text-align: right;">`;
|
||||
|
||||
const formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal);
|
||||
const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD');
|
||||
|
||||
@@ -290,7 +290,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
tooltip += `</div><span>${date}</span></div>`;
|
||||
tooltip += `</div></div>`;
|
||||
return tooltip;
|
||||
}.bind(this)
|
||||
},
|
||||
@@ -310,18 +310,21 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
formatter: (val): string => {
|
||||
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
|
||||
if (valSpan > 100_000_000_000) {
|
||||
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0)} BTC`;
|
||||
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0, undefined, true, true)} BTC`;
|
||||
}
|
||||
else if (valSpan > 1_000_000_000) {
|
||||
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2)} BTC`;
|
||||
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2, undefined, true, true)} BTC`;
|
||||
} else if (valSpan > 100_000_000) {
|
||||
return `${(val / 100_000_000).toFixed(1)} BTC`;
|
||||
} else if (valSpan > 10_000_000) {
|
||||
return `${(val / 100_000_000).toFixed(2)} BTC`;
|
||||
} else if (valSpan > 1_000_000) {
|
||||
if (maxValue > 100_000_000_000) {
|
||||
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 3, undefined, true, true)} BTC`;
|
||||
}
|
||||
return `${(val / 100_000_000).toFixed(3)} BTC`;
|
||||
} else {
|
||||
return `${this.amountShortenerPipe.transform(val, 0)} sats`;
|
||||
return `${this.amountShortenerPipe.transform(val, 0, undefined, true, true)} sats`;
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -335,7 +338,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: function(val) {
|
||||
return this.fiatShortenerPipe.transform(val, null, 'USD');
|
||||
return `$${this.amountShortenerPipe.transform(val, 0, undefined, true, true)}`;
|
||||
}.bind(this)
|
||||
},
|
||||
splitLine: {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
import { switchMap, filter, catchError, take } from 'rxjs/operators';
|
||||
import { Asset, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Asset, Transaction } from '@interfaces/electrs.interface';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { of, merge, Subscription, combineLatest } from 'rxjs';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { AssetsService } from '../../services/assets.service';
|
||||
import { moveDec } from '../../bitcoin.utils';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { environment } from '@environments/environment';
|
||||
import { AssetsService } from '@app/services/assets.service';
|
||||
import { moveDec } from '@app/bitcoin.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-asset',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FastVertexArray } from './fast-vertex-array';
|
||||
import TxView from './tx-view';
|
||||
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { Color, Position, Square, ViewUpdateParams } from './sprite-types';
|
||||
import { defaultColorFunction, contrastColorFunction } from './utils';
|
||||
import { ThemeService } from '../../services/theme.service';
|
||||
import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array';
|
||||
import TxView from '@components/block-overview-graph/tx-view';
|
||||
import { TransactionStripped } from '@interfaces/node-api.interface';
|
||||
import { Color, Position, Square, ViewUpdateParams } from '@components/block-overview-graph/sprite-types';
|
||||
import { defaultColorFunction, contrastColorFunction } from '@components/block-overview-graph/utils';
|
||||
import { ThemeService } from '@app/services/theme.service';
|
||||
|
||||
export default class BlockScene {
|
||||
scene: { count: number, offset: { x: number, y: number}};
|
||||
@@ -917,4 +917,4 @@ class BlockLayout {
|
||||
|
||||
function feeRateDescending(a: TxView, b: TxView) {
|
||||
return b.feerate - a.feerate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FastVertexArray } from './fast-vertex-array';
|
||||
import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from './sprite-types';
|
||||
import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array';
|
||||
import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from '@components/block-overview-graph/sprite-types';
|
||||
|
||||
const attribKeys = ['a', 'b', 't', 'v'];
|
||||
const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -2,13 +2,14 @@ import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, I
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { BehaviorSubject, combineLatest, Observable, timer, of, Subscription } from 'rxjs';
|
||||
import { debounceTime, delayWhen, filter, map, retryWhen, scan, skip, switchMap, tap, throttleTime } from 'rxjs/operators';
|
||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { BlockExtended } from '@interfaces/node-api.interface';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||
import { seoDescriptionNetwork } from '@app/shared/common.utils';
|
||||
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-blocks-list',
|
||||
@@ -49,6 +50,7 @@ export class BlocksList implements OnInit {
|
||||
private ogService: OpenGraphService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) {
|
||||
this.isMempoolModule = this.stateService.env.BASE_MODULE === 'mempool';
|
||||
@@ -182,7 +184,7 @@ export class BlocksList implements OnInit {
|
||||
}
|
||||
|
||||
pageChange(page: number): void {
|
||||
this.router.navigate(['blocks', page]);
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/blocks/'), page]);
|
||||
}
|
||||
|
||||
trackByBlock(index: number, block: BlockExtended): number {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<ng-template [ngIf]="button" [ngIfElse]="btnLink">
|
||||
<button #btn [attr.data-clipboard-text]="text" [class]="class" type="button" [disabled]="text === ''">
|
||||
<span #buttonWrapper [attr.data-tlite]="copiedMessage" style="position: relative;top: -2px;left: 1px;">
|
||||
<button [class]="class" type="button" [disabled]="text === ''" style="box-shadow: none;" (click)="copyText()">
|
||||
<span style="position: relative;top: -2px;left: 1px;">
|
||||
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images>
|
||||
<span *ngIf="showMessage" class="copied-message" style="top: 29px; left: -23.5px;">{{ copiedMessage }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #btnLink>
|
||||
<span #buttonWrapper [attr.data-tlite]="copiedMessage" style="position: relative;">
|
||||
<button #btn class="btn btn-sm btn-link pt-0 {{ leftPadding ? 'padding' : '' }}" [attr.data-clipboard-text]="text">
|
||||
<span style="position: relative;">
|
||||
<button class="btn btn-sm btn-link pt-0 {{ leftPadding ? 'padding' : '' }}" style="box-shadow: none;" (click)="copyText()">
|
||||
<app-svg-images name="clippy" [width]="widths[size]" viewBox="0 0 1000 1000"></app-svg-images>
|
||||
</button>
|
||||
<span *ngIf="showMessage" class="copied-message" style="top: 29px; left: -23.5px;">{{ copiedMessage }}</span>
|
||||
</span>
|
||||
</ng-template>
|
||||
|
||||
@@ -7,7 +7,19 @@
|
||||
padding-left: 0.4rem;
|
||||
}
|
||||
|
||||
img {
|
||||
position: relative;
|
||||
left: -3px;
|
||||
}
|
||||
.copied-message {
|
||||
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
|
||||
color: var(--fg);
|
||||
font-family: sans-serif;
|
||||
font-size: .8rem;
|
||||
font-weight: 400;
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
padding: .6em .75rem;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 .5rem 1rem -.5rem #000;
|
||||
z-index: 1000;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Component, ViewChild, ElementRef, AfterViewInit, Input, ChangeDetectionStrategy } from '@angular/core';
|
||||
import * as ClipboardJS from 'clipboard';
|
||||
import * as tlite from 'tlite';
|
||||
import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-clipboard',
|
||||
@@ -8,15 +6,14 @@ import * as tlite from 'tlite';
|
||||
styleUrls: ['./clipboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ClipboardComponent implements AfterViewInit {
|
||||
@ViewChild('btn') btn: ElementRef;
|
||||
@ViewChild('buttonWrapper') buttonWrapper: ElementRef;
|
||||
export class ClipboardComponent {
|
||||
@Input() button = false;
|
||||
@Input() class = 'btn btn-secondary ml-1';
|
||||
@Input() size: 'small' | 'normal' | 'large' = 'normal';
|
||||
@Input() text: string;
|
||||
@Input() leftPadding = true;
|
||||
copiedMessage: string = $localize`:@@clipboard.copied-message:Copied!`;
|
||||
showMessage = false;
|
||||
|
||||
widths = {
|
||||
small: '10',
|
||||
@@ -24,22 +21,40 @@ export class ClipboardComponent implements AfterViewInit {
|
||||
large: '18',
|
||||
};
|
||||
|
||||
clipboard: any;
|
||||
constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
) { }
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.clipboard = new ClipboardJS(this.btn.nativeElement);
|
||||
this.clipboard.on('success', () => {
|
||||
tlite.show(this.buttonWrapper.nativeElement);
|
||||
setTimeout(() => {
|
||||
tlite.hide(this.buttonWrapper.nativeElement);
|
||||
}, 1000);
|
||||
});
|
||||
async copyText() {
|
||||
if (this.text && !this.showMessage) {
|
||||
try {
|
||||
await this.copyToClipboard(this.text);
|
||||
this.showMessage = true;
|
||||
this.cd.markForCheck();
|
||||
setTimeout(() => {
|
||||
this.showMessage = false;
|
||||
this.cd.markForCheck();
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error('Clipboard copy failed:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
this.clipboard.destroy();
|
||||
async copyToClipboard(text: string) {
|
||||
if (navigator.clipboard) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
// Use the 'out of viewport hidden text area' trick on non-secure contexts
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = this.text;
|
||||
textarea.style.opacity = '0';
|
||||
textarea.setAttribute('readonly', 'true'); // Don't trigger keyboard on mobile
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
textarea.remove();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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> </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">
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { combineLatest, merge, Observable, of, Subject, Subscription } from 'rxjs';
|
||||
import { catchError, filter, map, scan, share, shareReplay, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { MempoolInfo, ReplacementInfo } from '../../interfaces/websocket.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { ActiveFilter, FilterMode, GradientMode, toFlags } from '../../shared/filters.utils';
|
||||
import { detectWebGL } from '../../shared/graphs.utils';
|
||||
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '@interfaces/node-api.interface';
|
||||
import { MempoolInfo, ReplacementInfo } from '@interfaces/websocket.interface';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { ActiveFilter, FilterMode, GradientMode, toFlags } from '@app/shared/filters.utils';
|
||||
import { detectWebGL } from '@app/shared/graphs.utils';
|
||||
import { Address, AddressTxSummary } from '@interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
|
||||
interface MempoolBlocksData {
|
||||
blocks: number;
|
||||
@@ -62,8 +62,10 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
|
||||
widgets;
|
||||
|
||||
addressSubscription: Subscription;
|
||||
walletSubscription: Subscription;
|
||||
blockTxSubscription: Subscription;
|
||||
addressSummary$: Observable<AddressTxSummary[]>;
|
||||
walletSummary$: Observable<AddressTxSummary[]>;
|
||||
address: Address;
|
||||
|
||||
goggleResolution = 82;
|
||||
@@ -71,7 +73,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
|
||||
{ index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' },
|
||||
{ index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' },
|
||||
{ index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
|
||||
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' },
|
||||
{ index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'fake_scripthash', 'op_return'], gradient: 'fee' },
|
||||
];
|
||||
goggleFlags = 0n;
|
||||
goggleMode: FilterMode = 'and';
|
||||
@@ -107,6 +109,10 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
|
||||
this.websocketService.stopTrackingAddress();
|
||||
this.address = null;
|
||||
}
|
||||
if (this.walletSubscription) {
|
||||
this.walletSubscription.unsubscribe();
|
||||
this.websocketService.stopTrackingWallet();
|
||||
}
|
||||
this.destroy$.next(1);
|
||||
this.destroy$.complete();
|
||||
}
|
||||
@@ -260,6 +266,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
|
||||
});
|
||||
|
||||
this.startAddressSubscription();
|
||||
this.startWalletSubscription();
|
||||
}
|
||||
|
||||
handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
|
||||
@@ -358,6 +365,75 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni
|
||||
}
|
||||
}
|
||||
|
||||
startWalletSubscription(): void {
|
||||
if (this.stateService.env.customize && this.stateService.env.customize.dashboard.widgets.some(w => w.props?.wallet)) {
|
||||
const walletName = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.wallet).props.wallet;
|
||||
this.websocketService.startTrackingWallet(walletName);
|
||||
|
||||
this.walletSummary$ = this.apiService.getWallet$(walletName).pipe(
|
||||
catchError(e => {
|
||||
return of({});
|
||||
}),
|
||||
switchMap(wallet => this.stateService.walletTransactions$.pipe(
|
||||
startWith([]),
|
||||
scan((summaries, newTransactions) => {
|
||||
const newSummaries: AddressTxSummary[] = [];
|
||||
for (const tx of newTransactions) {
|
||||
const funded: Record<string, number> = {};
|
||||
const spent: Record<string, number> = {};
|
||||
const fundedCount: Record<string, number> = {};
|
||||
const spentCount: Record<string, number> = {};
|
||||
for (const vin of tx.vin) {
|
||||
const address = vin.prevout?.scriptpubkey_address;
|
||||
if (address && wallet[address]) {
|
||||
spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0);
|
||||
spentCount[address] = (spentCount[address] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
const address = vout.scriptpubkey_address;
|
||||
if (address && wallet[address]) {
|
||||
funded[address] = (funded[address] ?? 0) + (vout.value ?? 0);
|
||||
fundedCount[address] = (fundedCount[address] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
for (const address of Object.keys({ ...funded, ...spent })) {
|
||||
// add tx to summary
|
||||
const txSummary: AddressTxSummary = {
|
||||
txid: tx.txid,
|
||||
value: (funded[address] ?? 0) - (spent[address] ?? 0),
|
||||
height: tx.status.block_height,
|
||||
time: tx.status.block_time,
|
||||
};
|
||||
wallet[address].transactions?.push(txSummary);
|
||||
newSummaries.push(txSummary);
|
||||
}
|
||||
}
|
||||
return this.deduplicateWalletTransactions([...summaries, ...newSummaries]);
|
||||
}, this.deduplicateWalletTransactions(Object.values(wallet).flatMap(address => address.transactions)))
|
||||
)),
|
||||
share(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
deduplicateWalletTransactions(walletTransactions: AddressTxSummary[]): AddressTxSummary[] {
|
||||
const transactions = new Map<string, AddressTxSummary>();
|
||||
for (const tx of walletTransactions) {
|
||||
if (transactions.has(tx.txid)) {
|
||||
transactions.get(tx.txid).value += tx.value;
|
||||
} else {
|
||||
transactions.set(tx.txid, tx);
|
||||
}
|
||||
}
|
||||
return Array.from(transactions.values()).sort((a, b) => {
|
||||
if (a.height === b.height) {
|
||||
return b.tx_position - a.tx_position;
|
||||
}
|
||||
return b.height - a.height;
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
if (window.innerWidth >= 992) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@ import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { catchError, startWith } from 'rxjs/operators';
|
||||
import { Subject, Subscription, of } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
|
||||
import { detectWebGL } from '../../shared/graphs.utils';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { BlockExtended, TransactionStripped } from '@interfaces/node-api.interface';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { BlockOverviewGraphComponent } from '@components/block-overview-graph/block-overview-graph.component';
|
||||
import { detectWebGL } from '@app/shared/graphs.utils';
|
||||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
|
||||
import { BytesPipe } from '@app/shared/pipes/bytes-pipe/bytes.pipe';
|
||||
|
||||
function bestFitResolution(min, max, n): number {
|
||||
const target = (min + max) / 2;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Component, OnDestroy, OnInit, ChangeDetectorRef } from "@angular/core";
|
||||
import { FormBuilder, FormGroup, Validators, ValidatorFn, AbstractControl, ValidationErrors } from "@angular/forms";
|
||||
import { Subscription } from "rxjs";
|
||||
import { ServicesApiServices } from "../../services/services-api.service";
|
||||
import { getRegex } from "../../shared/regex.utils";
|
||||
import { StateService } from "../../services/state.service";
|
||||
import { WebsocketService } from "../../services/websocket.service";
|
||||
import { AudioService } from "../../services/audio.service";
|
||||
import { HttpErrorResponse } from "@angular/common/http";
|
||||
import { Component, OnDestroy, OnInit, ChangeDetectorRef } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators, ValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { getRegex } from '@app/shared/regex.utils';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
@Component({
|
||||
selector: 'app-faucet',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user