Compare commits
1 Commits
v2.5.0-dev
...
hunicus/li
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9508bb88ef |
26
.github/workflows/get_image_digest.yml
vendored
26
.github/workflows/get_image_digest.yml
vendored
@@ -1,26 +0,0 @@
|
|||||||
name: 'Print images digest'
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: 'Image Version'
|
|
||||||
required: false
|
|
||||||
default: 'latest'
|
|
||||||
type: string
|
|
||||||
jobs:
|
|
||||||
print-images-sha:
|
|
||||||
runs-on: 'ubuntu-latest'
|
|
||||||
name: Print digest for images
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
path: digest
|
|
||||||
|
|
||||||
- name: Run script
|
|
||||||
working-directory: digest
|
|
||||||
run: |
|
|
||||||
sh ./docker/scripts/get_image_digest.sh $VERSION
|
|
||||||
env:
|
|
||||||
VERSION: ${{ github.event.inputs.version }}
|
|
||||||
@@ -171,58 +171,52 @@ Helpful link: https://gist.github.com/System-Glitch/cb4e87bf1ae3fec9925725bb3ebe
|
|||||||
|
|
||||||
Run bitcoind on regtest:
|
Run bitcoind on regtest:
|
||||||
```
|
```
|
||||||
bitcoind -regtest
|
bitcoind -regtest -rpcport=8332
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a new wallet, if needed:
|
Create a new wallet, if needed:
|
||||||
```
|
```
|
||||||
bitcoin-cli -regtest createwallet test
|
bitcoin-cli -regtest -rpcport=8332 createwallet test
|
||||||
```
|
```
|
||||||
|
|
||||||
Load wallet (this command may take a while if you have lot of UTXOs):
|
Load wallet (this command may take a while if you have lot of UTXOs):
|
||||||
```
|
```
|
||||||
bitcoin-cli -regtest loadwallet test
|
bitcoin-cli -regtest -rpcport=8332 loadwallet test
|
||||||
```
|
```
|
||||||
|
|
||||||
Get a new address:
|
Get a new address:
|
||||||
```
|
```
|
||||||
address=$(bitcoin-cli -regtest getnewaddress)
|
address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress)
|
||||||
```
|
```
|
||||||
|
|
||||||
Mine blocks to the previously generated address. You need at least 101 blocks before you can spend. This will take some time to execute (~1 min):
|
Mine blocks to the previously generated address. You need at least 101 blocks before you can spend. This will take some time to execute (~1 min):
|
||||||
```
|
```
|
||||||
bitcoin-cli -regtest generatetoaddress 101 $address
|
bitcoin-cli -regtest -rpcport=8332 generatetoaddress 101 $address
|
||||||
```
|
```
|
||||||
|
|
||||||
Send 0.1 BTC at 5 sat/vB to another address:
|
Send 0.1 BTC at 5 sat/vB to another address:
|
||||||
```
|
```
|
||||||
bitcoin-cli -named -regtest sendtoaddress address=$(bitcoin-cli -regtest getnewaddress) amount=0.1 fee_rate=5
|
./src/bitcoin-cli -named -regtest -rpcport=8332 sendtoaddress address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress) amount=0.1 fee_rate=5
|
||||||
```
|
```
|
||||||
|
|
||||||
See more example of `sendtoaddress`:
|
See more example of `sendtoaddress`:
|
||||||
```
|
```
|
||||||
bitcoin-cli sendtoaddress # will print the help
|
./src/bitcoin-cli sendtoaddress # will print the help
|
||||||
```
|
```
|
||||||
|
|
||||||
Mini script to generate random network activity (random TX count with random tx fee-rate). It's slow so don't expect to use this to test mempool spam, except if you let it run for a long time, or maybe with multiple regtest nodes connected to each other.
|
Mini script to generate transactions with random TX fee-rate (between 1 to 100 sat/vB). It's slow so don't expect to use this to test mempool spam, except if you let it run for a long time, or maybe with multiple regtest nodes connected to each other.
|
||||||
```
|
```
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
address=$(bitcoin-cli -regtest getnewaddress)
|
address=$(./src/bitcoin-cli -regtest -rpcport=8332 getnewaddress)
|
||||||
bitcoin-cli -regtest generatetoaddress 101 $address
|
|
||||||
for i in {1..1000000}
|
for i in {1..1000000}
|
||||||
do
|
do
|
||||||
for y in $(seq 1 "$(jot -r 1 1 1000)")
|
./src/bitcoin-cli -regtest -rpcport=8332 -named sendtoaddress address=$address amount=0.01 fee_rate=$(jot -r 1 1 100)
|
||||||
do
|
|
||||||
bitcoin-cli -regtest -named sendtoaddress address=$address amount=0.01 fee_rate=$(jot -r 1 1 100)
|
|
||||||
done
|
|
||||||
bitcoin-cli -regtest generatetoaddress 1 $address
|
|
||||||
sleep 5
|
|
||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
Generate block at regular interval (every 10 seconds in this example):
|
Generate block at regular interval (every 10 seconds in this example):
|
||||||
```
|
```
|
||||||
watch -n 10 "bitcoin-cli -regtest generatetoaddress 1 $address"
|
watch -n 10 "./src/bitcoin-cli -regtest -rpcport=8332 generatetoaddress 1 $address"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Mining pools update
|
### Mining pools update
|
||||||
|
|||||||
@@ -27,8 +27,7 @@
|
|||||||
"AUDIT": false,
|
"AUDIT": false,
|
||||||
"ADVANCED_GBT_AUDIT": false,
|
"ADVANCED_GBT_AUDIT": false,
|
||||||
"ADVANCED_GBT_MEMPOOL": false,
|
"ADVANCED_GBT_MEMPOOL": false,
|
||||||
"CPFP_INDEXING": false,
|
"CPFP_INDEXING": false
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 6
|
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
|
|||||||
@@ -28,8 +28,7 @@
|
|||||||
"ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__",
|
"ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__",
|
||||||
"ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__",
|
"ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__",
|
||||||
"CPFP_INDEXING": "__MEMPOOL_CPFP_INDEXING__",
|
"CPFP_INDEXING": "__MEMPOOL_CPFP_INDEXING__",
|
||||||
"MAX_BLOCKS_BULK_QUERY": "__MEMPOOL_MAX_BLOCKS_BULK_QUERY__",
|
"MAX_BLOCKS_BULK_QUERY": "__MEMPOOL_MAX_BLOCKS_BULK_QUERY__"
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": "__DISK_CACHE_BLOCK_INTERVAL__"
|
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "__CORE_RPC_HOST__",
|
"HOST": "__CORE_RPC_HOST__",
|
||||||
|
|||||||
@@ -23,11 +23,9 @@ describe('Mempool Difficulty Adjustment', () => {
|
|||||||
remainingBlocks: 1834,
|
remainingBlocks: 1834,
|
||||||
remainingTime: 977591692,
|
remainingTime: 977591692,
|
||||||
previousRetarget: 0.6280047707459726,
|
previousRetarget: 0.6280047707459726,
|
||||||
previousTime: 1660820820,
|
|
||||||
nextRetargetHeight: 751968,
|
nextRetargetHeight: 751968,
|
||||||
timeAvg: 533038,
|
timeAvg: 533038,
|
||||||
timeOffset: 0,
|
timeOffset: 0,
|
||||||
expectedBlocks: 161.68833333333333,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[ // Vector 2 (testnet)
|
[ // Vector 2 (testnet)
|
||||||
@@ -45,13 +43,11 @@ describe('Mempool Difficulty Adjustment', () => {
|
|||||||
estimatedRetargetDate: 1661895424692,
|
estimatedRetargetDate: 1661895424692,
|
||||||
remainingBlocks: 1834,
|
remainingBlocks: 1834,
|
||||||
remainingTime: 977591692,
|
remainingTime: 977591692,
|
||||||
previousTime: 1660820820,
|
|
||||||
previousRetarget: 0.6280047707459726,
|
previousRetarget: 0.6280047707459726,
|
||||||
nextRetargetHeight: 751968,
|
nextRetargetHeight: 751968,
|
||||||
timeAvg: 533038,
|
timeAvg: 533038,
|
||||||
timeOffset: -667000, // 11 min 7 seconds since last block (testnet only)
|
timeOffset: -667000, // 11 min 7 seconds since last block (testnet only)
|
||||||
// If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes
|
// If we add time avg to abs(timeOffset) it makes exactly 1200000 ms, or 20 minutes
|
||||||
expectedBlocks: 161.68833333333333,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
] as [[number, number, number, number, string, number], DifficultyAdjustment][];
|
] as [[number, number, number, number, string, number], DifficultyAdjustment][];
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ describe('Mempool Backend Config', () => {
|
|||||||
ADVANCED_GBT_MEMPOOL: false,
|
ADVANCED_GBT_MEMPOOL: false,
|
||||||
CPFP_INDEXING: false,
|
CPFP_INDEXING: false,
|
||||||
MAX_BLOCKS_BULK_QUERY: 0,
|
MAX_BLOCKS_BULK_QUERY: 0,
|
||||||
DISK_CACHE_BLOCK_INTERVAL: 6,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
|
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
|
||||||
@@ -107,13 +106,6 @@ describe('Mempool Backend Config', () => {
|
|||||||
BISQ_URL: 'https://bisq.markets/api',
|
BISQ_URL: 'https://bisq.markets/api',
|
||||||
BISQ_ONION: 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
|
BISQ_ONION: 'http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api'
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(config.MAXMIND).toStrictEqual({
|
|
||||||
ENABLED: false,
|
|
||||||
GEOLITE2_CITY: '/usr/local/share/GeoIP/GeoLite2-City.mmdb',
|
|
||||||
GEOLITE2_ASN: '/usr/local/share/GeoIP/GeoLite2-ASN.mmdb',
|
|
||||||
GEOIP2_ISP: '/usr/local/share/GeoIP/GeoIP2-ISP.mmdb'
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first
|
|||||||
|
|
||||||
class Audit {
|
class Audit {
|
||||||
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
|
auditBlock(transactions: TransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: TransactionExtended })
|
||||||
: { censored: string[], added: string[], fresh: string[], score: number, similarity: number } {
|
: { censored: string[], added: string[], fresh: string[], score: number } {
|
||||||
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||||
return { censored: [], added: [], fresh: [], score: 0, similarity: 1 };
|
return { censored: [], added: [], fresh: [], score: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const matches: string[] = []; // present in both mined block and template
|
const matches: string[] = []; // present in both mined block and template
|
||||||
@@ -16,8 +16,6 @@ class Audit {
|
|||||||
const isCensored = {}; // missing, without excuse
|
const isCensored = {}; // missing, without excuse
|
||||||
const isDisplaced = {};
|
const isDisplaced = {};
|
||||||
let displacedWeight = 0;
|
let displacedWeight = 0;
|
||||||
let matchedWeight = 0;
|
|
||||||
let projectedWeight = 0;
|
|
||||||
|
|
||||||
const inBlock = {};
|
const inBlock = {};
|
||||||
const inTemplate = {};
|
const inTemplate = {};
|
||||||
@@ -40,16 +38,11 @@ class Audit {
|
|||||||
isCensored[txid] = true;
|
isCensored[txid] = true;
|
||||||
}
|
}
|
||||||
displacedWeight += mempool[txid].weight;
|
displacedWeight += mempool[txid].weight;
|
||||||
} else {
|
|
||||||
matchedWeight += mempool[txid].weight;
|
|
||||||
}
|
}
|
||||||
projectedWeight += mempool[txid].weight;
|
|
||||||
inTemplate[txid] = true;
|
inTemplate[txid] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
displacedWeight += (4000 - transactions[0].weight);
|
displacedWeight += (4000 - transactions[0].weight);
|
||||||
projectedWeight += transactions[0].weight;
|
|
||||||
matchedWeight += transactions[0].weight;
|
|
||||||
|
|
||||||
// we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
|
// we can expect an honest miner to include 'displaced' transactions in place of recent arrivals and censored txs
|
||||||
// these displaced transactions should occupy the first N weight units of the next projected block
|
// these displaced transactions should occupy the first N weight units of the next projected block
|
||||||
@@ -128,14 +121,12 @@ class Audit {
|
|||||||
const numCensored = Object.keys(isCensored).length;
|
const numCensored = Object.keys(isCensored).length;
|
||||||
const numMatches = matches.length - 1; // adjust for coinbase tx
|
const numMatches = matches.length - 1; // adjust for coinbase tx
|
||||||
const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0;
|
const score = numMatches > 0 ? (numMatches / (numMatches + numCensored)) : 0;
|
||||||
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
censored: Object.keys(isCensored),
|
censored: Object.keys(isCensored),
|
||||||
added,
|
added,
|
||||||
fresh,
|
fresh,
|
||||||
score,
|
score
|
||||||
similarity,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,17 +220,18 @@ class BitcoinRoutes {
|
|||||||
let cpfpInfo;
|
let cpfpInfo;
|
||||||
if (config.DATABASE.ENABLED) {
|
if (config.DATABASE.ENABLED) {
|
||||||
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
|
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
|
||||||
}
|
|
||||||
if (cpfpInfo) {
|
|
||||||
res.json(cpfpInfo);
|
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
res.json({
|
res.json({
|
||||||
ancestors: []
|
ancestors: []
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (cpfpInfo) {
|
||||||
|
res.json(cpfpInfo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
res.status(404).send(`Transaction has no CPFP info available.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBackendInfo(req: Request, res: Response) {
|
private getBackendInfo(req: Request, res: Response) {
|
||||||
@@ -651,7 +652,7 @@ class BitcoinRoutes {
|
|||||||
if (result) {
|
if (result) {
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} else {
|
} else {
|
||||||
res.status(204).send();
|
res.status(404).send('not found');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
|||||||
@@ -143,10 +143,7 @@ class Blocks {
|
|||||||
* @returns BlockSummary
|
* @returns BlockSummary
|
||||||
*/
|
*/
|
||||||
public summarizeBlock(block: IBitcoinApi.VerboseBlock): BlockSummary {
|
public summarizeBlock(block: IBitcoinApi.VerboseBlock): BlockSummary {
|
||||||
if (Common.isLiquid()) {
|
const stripped = block.tx.map((tx) => {
|
||||||
block = this.convertLiquidFees(block);
|
|
||||||
}
|
|
||||||
const stripped = block.tx.map((tx: IBitcoinApi.VerboseTransaction) => {
|
|
||||||
return {
|
return {
|
||||||
txid: tx.txid,
|
txid: tx.txid,
|
||||||
vsize: tx.weight / 4,
|
vsize: tx.weight / 4,
|
||||||
@@ -161,13 +158,6 @@ class Blocks {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertLiquidFees(block: IBitcoinApi.VerboseBlock): IBitcoinApi.VerboseBlock {
|
|
||||||
block.tx.forEach(tx => {
|
|
||||||
tx.fee = Object.values(tx.fee || {}).reduce((total, output) => total + output, 0);
|
|
||||||
});
|
|
||||||
return block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a block with additional data (reward, coinbase, fees...)
|
* Return a block with additional data (reward, coinbase, fees...)
|
||||||
* @param block
|
* @param block
|
||||||
@@ -651,7 +641,7 @@ class Blocks {
|
|||||||
if (this.newBlockCallbacks.length) {
|
if (this.newBlockCallbacks.length) {
|
||||||
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
|
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
|
||||||
}
|
}
|
||||||
if (!memPool.hasPriority() && (block.height % config.MEMPOOL.DISK_CACHE_BLOCK_INTERVAL === 0)) {
|
if (!memPool.hasPriority()) {
|
||||||
diskCache.$saveCacheToDisk();
|
diskCache.$saveCacheToDisk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CpfpInfo, MempoolBlockWithTransactions, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
|
import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
||||||
import { isIP } from 'net';
|
import { isIP } from 'net';
|
||||||
@@ -164,30 +164,6 @@ export class Common {
|
|||||||
return parents;
|
return parents;
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculates the ratio of matched transactions to projected transactions by weight
|
|
||||||
static getSimilarity(projectedBlock: MempoolBlockWithTransactions, transactions: TransactionExtended[]): number {
|
|
||||||
let matchedWeight = 0;
|
|
||||||
let projectedWeight = 0;
|
|
||||||
const inBlock = {};
|
|
||||||
|
|
||||||
for (const tx of transactions) {
|
|
||||||
inBlock[tx.txid] = tx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// look for transactions that were expected in the template, but missing from the mined block
|
|
||||||
for (const tx of projectedBlock.transactions) {
|
|
||||||
if (inBlock[tx.txid]) {
|
|
||||||
matchedWeight += tx.vsize * 4;
|
|
||||||
}
|
|
||||||
projectedWeight += tx.vsize * 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
projectedWeight += transactions[0].weight;
|
|
||||||
matchedWeight += transactions[0].weight;
|
|
||||||
|
|
||||||
return projectedWeight ? matchedWeight / projectedWeight : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getSqlInterval(interval: string | null): string | null {
|
static getSqlInterval(interval: string | null): string | null {
|
||||||
switch (interval) {
|
switch (interval) {
|
||||||
case '24h': return '1 DAY';
|
case '24h': return '1 DAY';
|
||||||
@@ -199,7 +175,6 @@ export class Common {
|
|||||||
case '1y': return '1 YEAR';
|
case '1y': return '1 YEAR';
|
||||||
case '2y': return '2 YEAR';
|
case '2y': return '2 YEAR';
|
||||||
case '3y': return '3 YEAR';
|
case '3y': return '3 YEAR';
|
||||||
case '4y': return '4 YEAR';
|
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
|||||||
import { RowDataPacket } from 'mysql2';
|
import { RowDataPacket } from 'mysql2';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 59;
|
private static currentVersion = 58;
|
||||||
private queryTimeout = 3600_000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@@ -510,11 +510,6 @@ class DatabaseMigration {
|
|||||||
// We only run some migration queries for this version
|
// We only run some migration queries for this version
|
||||||
await this.updateToSchemaVersion(58);
|
await this.updateToSchemaVersion(58);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (databaseSchemaVersion < 59 && (config.MEMPOOL.NETWORK === 'signet' || config.MEMPOOL.NETWORK === 'testnet')) {
|
|
||||||
// https://github.com/mempool/mempool/issues/3360
|
|
||||||
await this.$executeQuery(`TRUNCATE prices`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1042,7 +1037,7 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('DELETE FROM `pools`');
|
await this.$executeQuery('DELETE FROM `pools`');
|
||||||
await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1');
|
await this.$executeQuery('ALTER TABLE pools AUTO_INCREMENT = 1');
|
||||||
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
|
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $convertCompactCpfpTables(): Promise<void> {
|
private async $convertCompactCpfpTables(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ export interface DifficultyAdjustment {
|
|||||||
remainingBlocks: number; // Block count
|
remainingBlocks: number; // Block count
|
||||||
remainingTime: number; // Duration of time in ms
|
remainingTime: number; // Duration of time in ms
|
||||||
previousRetarget: number; // Percent: -75 to 300
|
previousRetarget: number; // Percent: -75 to 300
|
||||||
previousTime: number; // Unix time in ms
|
|
||||||
nextRetargetHeight: number; // Block Height
|
nextRetargetHeight: number; // Block Height
|
||||||
timeAvg: number; // Duration of time in ms
|
timeAvg: number; // Duration of time in ms
|
||||||
timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms
|
timeOffset: number; // (Testnet) Time since last block (cap @ 20min) in ms
|
||||||
expectedBlocks: number; // Block count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calcDifficultyAdjustment(
|
export function calcDifficultyAdjustment(
|
||||||
@@ -34,12 +32,12 @@ export function calcDifficultyAdjustment(
|
|||||||
const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100;
|
const progressPercent = (blockHeight >= 0) ? blocksInEpoch / EPOCH_BLOCK_LENGTH * 100 : 100;
|
||||||
const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch;
|
const remainingBlocks = EPOCH_BLOCK_LENGTH - blocksInEpoch;
|
||||||
const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0;
|
const nextRetargetHeight = (blockHeight >= 0) ? blockHeight + remainingBlocks : 0;
|
||||||
const expectedBlocks = diffSeconds / BLOCK_SECONDS_TARGET;
|
|
||||||
|
|
||||||
let difficultyChange = 0;
|
let difficultyChange = 0;
|
||||||
let timeAvgSecs = diffSeconds / blocksInEpoch;
|
let timeAvgSecs = BLOCK_SECONDS_TARGET;
|
||||||
// Only calculate the estimate once we have 7.2% of blocks in current epoch
|
// Only calculate the estimate once we have 7.2% of blocks in current epoch
|
||||||
if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) {
|
if (blocksInEpoch >= ESTIMATE_LAG_BLOCKS) {
|
||||||
|
timeAvgSecs = diffSeconds / blocksInEpoch;
|
||||||
difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100;
|
difficultyChange = (BLOCK_SECONDS_TARGET / timeAvgSecs - 1) * 100;
|
||||||
// Max increase is x4 (+300%)
|
// Max increase is x4 (+300%)
|
||||||
if (difficultyChange > 300) {
|
if (difficultyChange > 300) {
|
||||||
@@ -76,11 +74,9 @@ export function calcDifficultyAdjustment(
|
|||||||
remainingBlocks,
|
remainingBlocks,
|
||||||
remainingTime,
|
remainingTime,
|
||||||
previousRetarget,
|
previousRetarget,
|
||||||
previousTime: DATime,
|
|
||||||
nextRetargetHeight,
|
nextRetargetHeight,
|
||||||
timeAvg,
|
timeAvg,
|
||||||
timeOffset,
|
timeOffset,
|
||||||
expectedBlocks,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,33 +11,23 @@ import { Common } from './common';
|
|||||||
class DiskCache {
|
class DiskCache {
|
||||||
private cacheSchemaVersion = 3;
|
private cacheSchemaVersion = 3;
|
||||||
|
|
||||||
private static TMP_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/tmp-cache.json';
|
|
||||||
private static TMP_FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/tmp-cache{number}.json';
|
|
||||||
private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json';
|
private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json';
|
||||||
private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json';
|
private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json';
|
||||||
private static CHUNK_FILES = 25;
|
private static CHUNK_FILES = 25;
|
||||||
private isWritingCache = false;
|
private isWritingCache = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() { }
|
||||||
if (!cluster.isPrimary) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
process.on('SIGINT', (e) => {
|
|
||||||
this.$saveCacheToDisk(true);
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async $saveCacheToDisk(sync: boolean = false): Promise<void> {
|
async $saveCacheToDisk(): Promise<void> {
|
||||||
if (!cluster.isPrimary) {
|
if (!cluster.isPrimary) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.isWritingCache) {
|
if (this.isWritingCache) {
|
||||||
logger.debug('Saving cache already in progress. Skipping.');
|
logger.debug('Saving cache already in progress. Skipping.')
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
logger.debug(`Writing mempool and blocks data to disk cache (${ sync ? 'sync' : 'async' })...`);
|
logger.debug('Writing mempool and blocks data to disk cache (async)...');
|
||||||
this.isWritingCache = true;
|
this.isWritingCache = true;
|
||||||
|
|
||||||
const mempool = memPool.getMempool();
|
const mempool = memPool.getMempool();
|
||||||
@@ -50,48 +40,19 @@ class DiskCache {
|
|||||||
|
|
||||||
const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES);
|
const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES);
|
||||||
|
|
||||||
if (sync) {
|
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
|
||||||
fs.writeFileSync(DiskCache.TMP_FILE_NAME, JSON.stringify({
|
cacheSchemaVersion: this.cacheSchemaVersion,
|
||||||
network: config.MEMPOOL.NETWORK,
|
blocks: blocks.getBlocks(),
|
||||||
cacheSchemaVersion: this.cacheSchemaVersion,
|
blockSummaries: blocks.getBlockSummaries(),
|
||||||
blocks: blocks.getBlocks(),
|
mempool: {},
|
||||||
blockSummaries: blocks.getBlockSummaries(),
|
mempoolArray: mempoolArray.splice(0, chunkSize),
|
||||||
|
}), { flag: 'w' });
|
||||||
|
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
|
||||||
|
await fsPromises.writeFile(DiskCache.FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
|
||||||
mempool: {},
|
mempool: {},
|
||||||
mempoolArray: mempoolArray.splice(0, chunkSize),
|
mempoolArray: mempoolArray.splice(0, chunkSize),
|
||||||
}), { flag: 'w' });
|
}), { flag: 'w' });
|
||||||
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
|
|
||||||
fs.writeFileSync(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
|
|
||||||
mempool: {},
|
|
||||||
mempoolArray: mempoolArray.splice(0, chunkSize),
|
|
||||||
}), { flag: 'w' });
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.renameSync(DiskCache.TMP_FILE_NAME, DiskCache.FILE_NAME);
|
|
||||||
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
|
|
||||||
fs.renameSync(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), DiskCache.FILE_NAMES.replace('{number}', i.toString()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await fsPromises.writeFile(DiskCache.TMP_FILE_NAME, JSON.stringify({
|
|
||||||
network: config.MEMPOOL.NETWORK,
|
|
||||||
cacheSchemaVersion: this.cacheSchemaVersion,
|
|
||||||
blocks: blocks.getBlocks(),
|
|
||||||
blockSummaries: blocks.getBlockSummaries(),
|
|
||||||
mempool: {},
|
|
||||||
mempoolArray: mempoolArray.splice(0, chunkSize),
|
|
||||||
}), { flag: 'w' });
|
|
||||||
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
|
|
||||||
await fsPromises.writeFile(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), JSON.stringify({
|
|
||||||
mempool: {},
|
|
||||||
mempoolArray: mempoolArray.splice(0, chunkSize),
|
|
||||||
}), { flag: 'w' });
|
|
||||||
}
|
|
||||||
|
|
||||||
await fsPromises.rename(DiskCache.TMP_FILE_NAME, DiskCache.FILE_NAME);
|
|
||||||
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
|
|
||||||
await fsPromises.rename(DiskCache.TMP_FILE_NAMES.replace('{number}', i.toString()), DiskCache.FILE_NAMES.replace('{number}', i.toString()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('Mempool and blocks data saved to disk cache');
|
logger.debug('Mempool and blocks data saved to disk cache');
|
||||||
this.isWritingCache = false;
|
this.isWritingCache = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -100,8 +61,8 @@ class DiskCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wipeCache(): void {
|
wipeCache() {
|
||||||
logger.notice(`Wiping nodejs backend cache/cache*.json files`);
|
logger.notice(`Wipping nodejs backend cache/cache*.json files`);
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(DiskCache.FILE_NAME);
|
fs.unlinkSync(DiskCache.FILE_NAME);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -122,7 +83,7 @@ class DiskCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMempoolCache(): void {
|
loadMempoolCache() {
|
||||||
if (!fs.existsSync(DiskCache.FILE_NAME)) {
|
if (!fs.existsSync(DiskCache.FILE_NAME)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -136,10 +97,6 @@ class DiskCache {
|
|||||||
logger.notice('Disk cache contains an outdated schema version. Clearing it and skipping the cache loading.');
|
logger.notice('Disk cache contains an outdated schema version. Clearing it and skipping the cache loading.');
|
||||||
return this.wipeCache();
|
return this.wipeCache();
|
||||||
}
|
}
|
||||||
if (data.network && data.network !== config.MEMPOOL.NETWORK) {
|
|
||||||
logger.notice('Disk cache contains data from a different network. Clearing it and skipping the cache loading.');
|
|
||||||
return this.wipeCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.mempoolArray) {
|
if (data.mempoolArray) {
|
||||||
for (const tx of data.mempoolArray) {
|
for (const tx of data.mempoolArray) {
|
||||||
|
|||||||
@@ -417,24 +417,24 @@ class NodesApi {
|
|||||||
|
|
||||||
if (!ispList[isp1]) {
|
if (!ispList[isp1]) {
|
||||||
ispList[isp1] = {
|
ispList[isp1] = {
|
||||||
ids: [channel.isp1ID],
|
id: channel.isp1ID.toString(),
|
||||||
capacity: 0,
|
capacity: 0,
|
||||||
channels: 0,
|
channels: 0,
|
||||||
nodes: {},
|
nodes: {},
|
||||||
};
|
};
|
||||||
} else if (ispList[isp1].ids.includes(channel.isp1ID) === false) {
|
} else if (ispList[isp1].id.indexOf(channel.isp1ID) === -1) {
|
||||||
ispList[isp1].ids.push(channel.isp1ID);
|
ispList[isp1].id += ',' + channel.isp1ID.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ispList[isp2]) {
|
if (!ispList[isp2]) {
|
||||||
ispList[isp2] = {
|
ispList[isp2] = {
|
||||||
ids: [channel.isp2ID],
|
id: channel.isp2ID.toString(),
|
||||||
capacity: 0,
|
capacity: 0,
|
||||||
channels: 0,
|
channels: 0,
|
||||||
nodes: {},
|
nodes: {},
|
||||||
};
|
};
|
||||||
} else if (ispList[isp2].ids.includes(channel.isp2ID) === false) {
|
} else if (ispList[isp2].id.indexOf(channel.isp2ID) === -1) {
|
||||||
ispList[isp2].ids.push(channel.isp2ID);
|
ispList[isp2].id += ',' + channel.isp2ID.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
ispList[isp1].capacity += channel.capacity;
|
ispList[isp1].capacity += channel.capacity;
|
||||||
@@ -444,11 +444,11 @@ class NodesApi {
|
|||||||
ispList[isp2].channels += 1;
|
ispList[isp2].channels += 1;
|
||||||
ispList[isp2].nodes[channel.node2PublicKey] = true;
|
ispList[isp2].nodes[channel.node2PublicKey] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ispRanking: any[] = [];
|
const ispRanking: any[] = [];
|
||||||
for (const isp of Object.keys(ispList)) {
|
for (const isp of Object.keys(ispList)) {
|
||||||
ispRanking.push([
|
ispRanking.push([
|
||||||
ispList[isp].ids.sort((a, b) => a - b).join(','),
|
ispList[isp].id,
|
||||||
isp,
|
isp,
|
||||||
ispList[isp].capacity,
|
ispList[isp].capacity,
|
||||||
ispList[isp].channels,
|
ispList[isp].channels,
|
||||||
|
|||||||
@@ -31,11 +31,6 @@ class Mempool {
|
|||||||
private mempoolProtection = 0;
|
private mempoolProtection = 0;
|
||||||
private latestTransactions: any[] = [];
|
private latestTransactions: any[] = [];
|
||||||
|
|
||||||
private ESPLORA_MISSING_TX_WARNING_THRESHOLD = 100;
|
|
||||||
private SAMPLE_TIME = 10000; // In ms
|
|
||||||
private timer = new Date().getTime();
|
|
||||||
private missingTxCount = 0;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
setInterval(this.updateTxPerSecond.bind(this), 1000);
|
setInterval(this.updateTxPerSecond.bind(this), 1000);
|
||||||
setInterval(this.deleteExpiredTransactions.bind(this), 20000);
|
setInterval(this.deleteExpiredTransactions.bind(this), 20000);
|
||||||
@@ -133,16 +128,6 @@ class Mempool {
|
|||||||
loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100);
|
loadingIndicators.setProgress('mempool', Object.keys(this.mempoolCache).length / transactions.length * 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/mempool/mempool/issues/3283
|
|
||||||
const logEsplora404 = (missingTxCount, threshold, time) => {
|
|
||||||
const log = `In the past ${time / 1000} seconds, esplora tx API replied ${missingTxCount} times with a 404 error code while updating nodejs backend mempool`;
|
|
||||||
if (missingTxCount >= threshold) {
|
|
||||||
logger.warn(log);
|
|
||||||
} else if (missingTxCount > 0) {
|
|
||||||
logger.debug(log);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const txid of transactions) {
|
for (const txid of transactions) {
|
||||||
if (!this.mempoolCache[txid]) {
|
if (!this.mempoolCache[txid]) {
|
||||||
try {
|
try {
|
||||||
@@ -157,10 +142,7 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
hasChange = true;
|
hasChange = true;
|
||||||
newTransactions.push(transaction);
|
newTransactions.push(transaction);
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora' && e.response?.status === 404) {
|
|
||||||
this.missingTxCount++;
|
|
||||||
}
|
|
||||||
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
|
logger.debug(`Error finding transaction '${txid}' in the mempool: ` + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,14 +152,6 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset esplora 404 counter and log a warning if needed
|
|
||||||
const elapsedTime = new Date().getTime() - this.timer;
|
|
||||||
if (elapsedTime > this.SAMPLE_TIME) {
|
|
||||||
logEsplora404(this.missingTxCount, this.ESPLORA_MISSING_TX_WARNING_THRESHOLD, elapsedTime);
|
|
||||||
this.timer = new Date().getTime();
|
|
||||||
this.missingTxCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent mempool from clear on bitcoind restart by delaying the deletion
|
// Prevent mempool from clear on bitcoind restart by delaying the deletion
|
||||||
if (this.mempoolProtection === 0
|
if (this.mempoolProtection === 0
|
||||||
&& currentMempoolSize > 20000
|
&& currentMempoolSize > 20000
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ class MiningRoutes {
|
|||||||
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
|
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
|
||||||
|
|
||||||
if (!audit) {
|
if (!audit) {
|
||||||
res.status(204).send(`This block has not been audited.`);
|
res.status(404).send(`This block has not been audited.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import BlocksAuditsRepository from '../../repositories/BlocksAuditsRepository';
|
|||||||
import PricesRepository from '../../repositories/PricesRepository';
|
import PricesRepository from '../../repositories/PricesRepository';
|
||||||
import { bitcoinCoreApi } from '../bitcoin/bitcoin-api-factory';
|
import { bitcoinCoreApi } from '../bitcoin/bitcoin-api-factory';
|
||||||
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
|
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
|
||||||
import database from '../../database';
|
|
||||||
|
|
||||||
class Mining {
|
class Mining {
|
||||||
private blocksPriceIndexingRunning = false;
|
private blocksPriceIndexingRunning = false;
|
||||||
@@ -118,7 +117,7 @@ class Mining {
|
|||||||
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
|
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
poolsStatistics['lastEstimatedHashrate'] = 0;
|
poolsStatistics['lastEstimatedHashrate'] = 0;
|
||||||
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);
|
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate');
|
||||||
}
|
}
|
||||||
|
|
||||||
return poolsStatistics;
|
return poolsStatistics;
|
||||||
@@ -142,14 +141,11 @@ class Mining {
|
|||||||
const blockCount1w: number = await BlocksRepository.$blockCount(pool.id, '1w');
|
const blockCount1w: number = await BlocksRepository.$blockCount(pool.id, '1w');
|
||||||
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
|
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
|
||||||
|
|
||||||
const avgHealth = await BlocksRepository.$getAvgBlockHealthPerPoolId(pool.id);
|
|
||||||
const totalReward = await BlocksRepository.$getTotalRewardForPoolId(pool.id);
|
|
||||||
|
|
||||||
let currentEstimatedHashrate = 0;
|
let currentEstimatedHashrate = 0;
|
||||||
try {
|
try {
|
||||||
currentEstimatedHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h);
|
currentEstimatedHashrate = await bitcoinClient.getNetworkHashPs(totalBlock24h);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);
|
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -166,8 +162,6 @@ class Mining {
|
|||||||
},
|
},
|
||||||
estimatedHashrate: currentEstimatedHashrate * (blockCount24h / totalBlock24h),
|
estimatedHashrate: currentEstimatedHashrate * (blockCount24h / totalBlock24h),
|
||||||
reportedHashrate: null,
|
reportedHashrate: null,
|
||||||
avgBlockHealth: avgHealth,
|
|
||||||
totalReward: totalReward,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +208,7 @@ class Mining {
|
|||||||
const startedAt = new Date().getTime() / 1000;
|
const startedAt = new Date().getTime() / 1000;
|
||||||
let timer = new Date().getTime() / 1000;
|
let timer = new Date().getTime() / 1000;
|
||||||
|
|
||||||
logger.debug(`Indexing weekly mining pool hashrate`, logger.tags.mining);
|
logger.debug(`Indexing weekly mining pool hashrate`);
|
||||||
loadingIndicators.setProgress('weekly-hashrate-indexing', 0);
|
loadingIndicators.setProgress('weekly-hashrate-indexing', 0);
|
||||||
|
|
||||||
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
|
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
|
||||||
@@ -251,7 +245,7 @@ class Mining {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
newlyIndexed += hashrates.length / Math.max(1, pools.length);
|
newlyIndexed += hashrates.length;
|
||||||
await HashratesRepository.$saveHashrates(hashrates);
|
await HashratesRepository.$saveHashrates(hashrates);
|
||||||
hashrates.length = 0;
|
hashrates.length = 0;
|
||||||
}
|
}
|
||||||
@@ -262,7 +256,7 @@ class Mining {
|
|||||||
const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
|
const weeksPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
|
||||||
const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100;
|
const progress = Math.round(totalIndexed / totalWeekIndexed * 10000) / 100;
|
||||||
const formattedDate = new Date(fromTimestamp).toUTCString();
|
const formattedDate = new Date(fromTimestamp).toUTCString();
|
||||||
logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
|
logger.debug(`Getting weekly pool hashrate for ${formattedDate} | ~${weeksPerSeconds.toFixed(2)} weeks/sec | total: ~${totalIndexed}/${Math.round(totalWeekIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||||
timer = new Date().getTime() / 1000;
|
timer = new Date().getTime() / 1000;
|
||||||
indexedThisRun = 0;
|
indexedThisRun = 0;
|
||||||
loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false);
|
loadingIndicators.setProgress('weekly-hashrate-indexing', progress, false);
|
||||||
@@ -274,14 +268,14 @@ class Mining {
|
|||||||
}
|
}
|
||||||
this.lastWeeklyHashrateIndexingDate = new Date().getUTCDate();
|
this.lastWeeklyHashrateIndexingDate = new Date().getUTCDate();
|
||||||
if (newlyIndexed > 0) {
|
if (newlyIndexed > 0) {
|
||||||
logger.info(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed} weeks`, logger.tags.mining);
|
logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed} weeks`, logger.tags.mining);
|
logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
|
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
|
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
|
||||||
logger.err(`Weekly mining pools hashrates indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
|
logger.err(`Weekly mining pools hashrates indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,7 +308,7 @@ class Mining {
|
|||||||
const startedAt = new Date().getTime() / 1000;
|
const startedAt = new Date().getTime() / 1000;
|
||||||
let timer = new Date().getTime() / 1000;
|
let timer = new Date().getTime() / 1000;
|
||||||
|
|
||||||
logger.debug(`Indexing daily network hashrate`, logger.tags.mining);
|
logger.debug(`Indexing daily network hashrate`);
|
||||||
loadingIndicators.setProgress('daily-hashrate-indexing', 0);
|
loadingIndicators.setProgress('daily-hashrate-indexing', 0);
|
||||||
|
|
||||||
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
|
while (toTimestamp > genesisTimestamp && toTimestamp > oldestConsecutiveBlockTimestamp) {
|
||||||
@@ -352,7 +346,7 @@ class Mining {
|
|||||||
const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
|
const daysPerSeconds = Math.max(1, Math.round(indexedThisRun / elapsedSeconds));
|
||||||
const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100;
|
const progress = Math.round(totalIndexed / totalDayIndexed * 10000) / 100;
|
||||||
const formattedDate = new Date(fromTimestamp).toUTCString();
|
const formattedDate = new Date(fromTimestamp).toUTCString();
|
||||||
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
|
logger.debug(`Getting network daily hashrate for ${formattedDate} | ~${daysPerSeconds.toFixed(2)} days/sec | total: ~${totalIndexed}/${Math.round(totalDayIndexed)} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||||
timer = new Date().getTime() / 1000;
|
timer = new Date().getTime() / 1000;
|
||||||
indexedThisRun = 0;
|
indexedThisRun = 0;
|
||||||
loadingIndicators.setProgress('daily-hashrate-indexing', progress);
|
loadingIndicators.setProgress('daily-hashrate-indexing', progress);
|
||||||
@@ -379,14 +373,14 @@ class Mining {
|
|||||||
|
|
||||||
this.lastHashrateIndexingDate = new Date().getUTCDate();
|
this.lastHashrateIndexingDate = new Date().getUTCDate();
|
||||||
if (newlyIndexed > 0) {
|
if (newlyIndexed > 0) {
|
||||||
logger.info(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining);
|
logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining);
|
logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
|
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
|
loadingIndicators.setProgress('daily-hashrate-indexing', 100);
|
||||||
logger.err(`Daily network hashrate indexing failed. Trying again later. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
|
logger.err(`Daily network hashrate indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,13 +446,13 @@ class Mining {
|
|||||||
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
|
||||||
if (elapsedSeconds > 5) {
|
if (elapsedSeconds > 5) {
|
||||||
const progress = Math.round(totalBlockChecked / blocks.length * 100);
|
const progress = Math.round(totalBlockChecked / blocks.length * 100);
|
||||||
logger.info(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`, logger.tags.mining);
|
logger.info(`Indexing difficulty adjustment at block #${block.height} | Progress: ${progress}%`);
|
||||||
timer = new Date().getTime() / 1000;
|
timer = new Date().getTime() / 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalIndexed > 0) {
|
if (totalIndexed > 0) {
|
||||||
logger.info(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
|
logger.notice(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
|
logger.debug(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
@@ -505,7 +499,7 @@ class Mining {
|
|||||||
if (blocksWithoutPrices.length > 200000) {
|
if (blocksWithoutPrices.length > 200000) {
|
||||||
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
|
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
|
||||||
}
|
}
|
||||||
logger.debug(logStr, logger.tags.mining);
|
logger.debug(logStr);
|
||||||
await BlocksRepository.$saveBlockPrices(blocksPrices);
|
await BlocksRepository.$saveBlockPrices(blocksPrices);
|
||||||
blocksPrices.length = 0;
|
blocksPrices.length = 0;
|
||||||
}
|
}
|
||||||
@@ -517,7 +511,7 @@ class Mining {
|
|||||||
if (blocksWithoutPrices.length > 200000) {
|
if (blocksWithoutPrices.length > 200000) {
|
||||||
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
|
logStr += ` | Progress ${Math.round(totalInserted / blocksWithoutPrices.length * 100)}%`;
|
||||||
}
|
}
|
||||||
logger.debug(logStr, logger.tags.mining);
|
logger.debug(logStr);
|
||||||
await BlocksRepository.$saveBlockPrices(blocksPrices);
|
await BlocksRepository.$saveBlockPrices(blocksPrices);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -574,7 +568,6 @@ class Mining {
|
|||||||
|
|
||||||
private getTimeRange(interval: string | null, scale = 1): number {
|
private getTimeRange(interval: string | null, scale = 1): number {
|
||||||
switch (interval) {
|
switch (interval) {
|
||||||
case '4y': return 43200 * scale; // 12h
|
|
||||||
case '3y': return 43200 * scale; // 12h
|
case '3y': return 43200 * scale; // 12h
|
||||||
case '2y': return 28800 * scale; // 8h
|
case '2y': return 28800 * scale; // 8h
|
||||||
case '1y': return 28800 * scale; // 8h
|
case '1y': return 28800 * scale; // 8h
|
||||||
|
|||||||
@@ -375,17 +375,6 @@ class StatisticsApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $list4Y(): Promise<OptimizedStatistic[]> {
|
|
||||||
try {
|
|
||||||
const query = this.getQueryForDays(43200, '4 YEAR'); // 12h interval
|
|
||||||
const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
|
|
||||||
return this.mapStatisticToOptimizedStatistic(rows as Statistic[]);
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('$list4Y() error' + (e instanceof Error ? e.message : e));
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] {
|
private mapStatisticToOptimizedStatistic(statistic: Statistic[]): OptimizedStatistic[] {
|
||||||
return statistic.map((s) => {
|
return statistic.map((s) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -14,11 +14,10 @@ class StatisticsRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', this.$getStatisticsByTime.bind(this, '1y'))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', this.$getStatisticsByTime.bind(this, '1y'))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y'))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', this.$getStatisticsByTime.bind(this, '2y'))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y'))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', this.$getStatisticsByTime.bind(this, '3y'))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/4y', this.$getStatisticsByTime.bind(this, '4y'))
|
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y' | '4y', req: Request, res: Response) {
|
private async $getStatisticsByTime(time: '2h' | '24h' | '1w' | '1m' | '3m' | '6m' | '1y' | '2y' | '3y', req: Request, res: Response) {
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
@@ -55,9 +54,6 @@ class StatisticsRoutes {
|
|||||||
case '3y':
|
case '3y':
|
||||||
result = await statisticsApi.$list3Y();
|
result = await statisticsApi.$list3Y();
|
||||||
break;
|
break;
|
||||||
case '4y':
|
|
||||||
result = await statisticsApi.$list4Y();
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
result = await statisticsApi.$list2H();
|
result = await statisticsApi.$list2H();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import * as WebSocket from 'ws';
|
import * as WebSocket from 'ws';
|
||||||
import {
|
import {
|
||||||
BlockExtended, TransactionExtended, WebsocketResponse,
|
BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta,
|
||||||
OptimizedStatistic, ILoadingIndicators
|
OptimizedStatistic, ILoadingIndicators, IConversionRates
|
||||||
} from '../mempool.interfaces';
|
} from '../mempool.interfaces';
|
||||||
import blocks from './blocks';
|
import blocks from './blocks';
|
||||||
import memPool from './mempool';
|
import memPool from './mempool';
|
||||||
@@ -20,7 +20,6 @@ import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository
|
|||||||
import Audit from './audit';
|
import Audit from './audit';
|
||||||
import { deepClone } from '../utils/clone';
|
import { deepClone } from '../utils/clone';
|
||||||
import priceUpdater from '../tasks/price-updater';
|
import priceUpdater from '../tasks/price-updater';
|
||||||
import { ApiPrice } from '../repositories/PricesRepository';
|
|
||||||
|
|
||||||
class WebsocketHandler {
|
class WebsocketHandler {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@@ -194,7 +193,7 @@ class WebsocketHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewConversionRates(conversionRates: ApiPrice) {
|
handleNewConversionRates(conversionRates: IConversionRates) {
|
||||||
if (!this.wss) {
|
if (!this.wss) {
|
||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
@@ -215,7 +214,7 @@ class WebsocketHandler {
|
|||||||
'mempoolInfo': memPool.getMempoolInfo(),
|
'mempoolInfo': memPool.getMempoolInfo(),
|
||||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||||
'blocks': _blocks,
|
'blocks': _blocks,
|
||||||
'conversions': priceUpdater.getLatestPrices(),
|
'conversions': priceUpdater.latestPrices,
|
||||||
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
||||||
'transactions': memPool.getLatestTransactions(),
|
'transactions': memPool.getLatestTransactions(),
|
||||||
'backendInfo': backendInfo.getBackendInfo(),
|
'backendInfo': backendInfo.getBackendInfo(),
|
||||||
@@ -432,7 +431,7 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Common.indexingEnabled() && memPool.isInSync()) {
|
if (Common.indexingEnabled() && memPool.isInSync()) {
|
||||||
const { censored, added, fresh, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||||
const matchRate = Math.round(score * 100 * 100) / 100;
|
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||||
|
|
||||||
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
|
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
|
||||||
@@ -464,14 +463,8 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
if (block.extras) {
|
if (block.extras) {
|
||||||
block.extras.matchRate = matchRate;
|
block.extras.matchRate = matchRate;
|
||||||
block.extras.similarity = similarity;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (block.extras) {
|
|
||||||
const mBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
|
||||||
if (mBlocks?.length && mBlocks[0].transactions) {
|
|
||||||
block.extras.similarity = Common.getSimilarity(mBlocks[0], transactions);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const removed: string[] = [];
|
const removed: string[] = [];
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ interface IConfig {
|
|||||||
ADVANCED_GBT_MEMPOOL: boolean;
|
ADVANCED_GBT_MEMPOOL: boolean;
|
||||||
CPFP_INDEXING: boolean;
|
CPFP_INDEXING: boolean;
|
||||||
MAX_BLOCKS_BULK_QUERY: number;
|
MAX_BLOCKS_BULK_QUERY: number;
|
||||||
DISK_CACHE_BLOCK_INTERVAL: number;
|
|
||||||
};
|
};
|
||||||
ESPLORA: {
|
ESPLORA: {
|
||||||
REST_API_URL: string;
|
REST_API_URL: string;
|
||||||
@@ -156,7 +155,6 @@ const defaults: IConfig = {
|
|||||||
'ADVANCED_GBT_MEMPOOL': false,
|
'ADVANCED_GBT_MEMPOOL': false,
|
||||||
'CPFP_INDEXING': false,
|
'CPFP_INDEXING': false,
|
||||||
'MAX_BLOCKS_BULK_QUERY': 0,
|
'MAX_BLOCKS_BULK_QUERY': 0,
|
||||||
'DISK_CACHE_BLOCK_INTERVAL': 6,
|
|
||||||
},
|
},
|
||||||
'ESPLORA': {
|
'ESPLORA': {
|
||||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ import forensicsService from './tasks/lightning/forensics.service';
|
|||||||
import priceUpdater from './tasks/price-updater';
|
import priceUpdater from './tasks/price-updater';
|
||||||
import chainTips from './api/chain-tips';
|
import chainTips from './api/chain-tips';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import v8 from 'v8';
|
|
||||||
import { formatBytes, getBytesUnit } from './utils/format';
|
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@@ -47,11 +45,6 @@ class Server {
|
|||||||
private app: Application;
|
private app: Application;
|
||||||
private currentBackendRetryInterval = 5;
|
private currentBackendRetryInterval = 5;
|
||||||
|
|
||||||
private maxHeapSize: number = 0;
|
|
||||||
private heapLogInterval: number = 60;
|
|
||||||
private warnedHeapCritical: boolean = false;
|
|
||||||
private lastHeapLogTime: number | null = null;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.app = express();
|
this.app = express();
|
||||||
|
|
||||||
@@ -144,8 +137,6 @@ class Server {
|
|||||||
this.runMainUpdateLoop();
|
this.runMainUpdateLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(() => { this.healthCheck(); }, 2500);
|
|
||||||
|
|
||||||
if (config.BISQ.ENABLED) {
|
if (config.BISQ.ENABLED) {
|
||||||
bisq.startBisqService();
|
bisq.startBisqService();
|
||||||
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
|
bisq.setPriceCallbackFunction((price) => websocketHandler.setExtraInitProperties('bsq-price', price));
|
||||||
@@ -264,26 +255,6 @@ class Server {
|
|||||||
channelsRoutes.initRoutes(this.app);
|
channelsRoutes.initRoutes(this.app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
healthCheck(): void {
|
|
||||||
const now = Date.now();
|
|
||||||
const stats = v8.getHeapStatistics();
|
|
||||||
this.maxHeapSize = Math.max(stats.used_heap_size, this.maxHeapSize);
|
|
||||||
const warnThreshold = 0.8 * stats.heap_size_limit;
|
|
||||||
|
|
||||||
const byteUnits = getBytesUnit(Math.max(this.maxHeapSize, stats.heap_size_limit));
|
|
||||||
|
|
||||||
if (!this.warnedHeapCritical && this.maxHeapSize > warnThreshold) {
|
|
||||||
this.warnedHeapCritical = true;
|
|
||||||
logger.warn(`Used ${(this.maxHeapSize / stats.heap_size_limit).toFixed(2)}% of heap limit (${formatBytes(this.maxHeapSize, byteUnits, true)} / ${formatBytes(stats.heap_size_limit, byteUnits)})!`);
|
|
||||||
}
|
|
||||||
if (this.lastHeapLogTime === null || (now - this.lastHeapLogTime) > (this.heapLogInterval * 1000)) {
|
|
||||||
logger.debug(`Memory usage: ${formatBytes(this.maxHeapSize, byteUnits)} / ${formatBytes(stats.heap_size_limit, byteUnits)}`);
|
|
||||||
this.warnedHeapCritical = false;
|
|
||||||
this.maxHeapSize = 0;
|
|
||||||
this.lastHeapLogTime = now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
((): Server => new Server())();
|
((): Server => new Server())();
|
||||||
|
|||||||
@@ -76,13 +76,13 @@ class Indexer {
|
|||||||
this.tasksRunning.push(task);
|
this.tasksRunning.push(task);
|
||||||
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||||
if (priceUpdater.historyInserted === false || lastestPriceId === null) {
|
if (priceUpdater.historyInserted === false || lastestPriceId === null) {
|
||||||
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`, logger.tags.mining);
|
logger.debug(`Blocks prices indexer is waiting for the price updater to complete`);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
|
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
|
||||||
this.runSingleTask('blocksPrices');
|
this.runSingleTask('blocksPrices');
|
||||||
}, 10000);
|
}, 10000);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Blocks prices indexer will run now`, logger.tags.mining);
|
logger.debug(`Blocks prices indexer will run now`);
|
||||||
await mining.$indexBlockPrices();
|
await mining.$indexBlockPrices();
|
||||||
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
|
this.tasksRunning = this.tasksRunning.filter(runningTask => runningTask !== task);
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ class Indexer {
|
|||||||
this.runIndexer = false;
|
this.runIndexer = false;
|
||||||
this.indexerRunning = true;
|
this.indexerRunning = true;
|
||||||
|
|
||||||
logger.debug(`Running mining indexer`);
|
logger.info(`Running mining indexer`);
|
||||||
|
|
||||||
await this.checkAvailableCoreIndexes();
|
await this.checkAvailableCoreIndexes();
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ class Indexer {
|
|||||||
const chainValid = await blocks.$generateBlockDatabase();
|
const chainValid = await blocks.$generateBlockDatabase();
|
||||||
if (chainValid === false) {
|
if (chainValid === false) {
|
||||||
// Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration
|
// Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration
|
||||||
logger.warn(`The chain of block hash is invalid, re-indexing invalid data in 10 seconds.`, logger.tags.mining);
|
logger.warn(`The chain of block hash is invalid, re-indexing invalid data in 10 seconds.`);
|
||||||
setTimeout(() => this.reindex(), 10000);
|
setTimeout(() => this.reindex(), 10000);
|
||||||
this.indexerRunning = false;
|
this.indexerRunning = false;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -153,7 +153,6 @@ export interface BlockExtension {
|
|||||||
feeRange: number[]; // fee rate percentiles
|
feeRange: number[]; // fee rate percentiles
|
||||||
reward: number;
|
reward: number;
|
||||||
matchRate: number | null;
|
matchRate: number | null;
|
||||||
similarity?: number;
|
|
||||||
pool: {
|
pool: {
|
||||||
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
|
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
|
||||||
name: string;
|
name: string;
|
||||||
@@ -294,6 +293,7 @@ interface RequiredParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ILoadingIndicators { [name: string]: number; }
|
export interface ILoadingIndicators { [name: string]: number; }
|
||||||
|
export interface IConversionRates { [currency: string]: number; }
|
||||||
|
|
||||||
export interface IBackendInfo {
|
export interface IBackendInfo {
|
||||||
hostname: string;
|
hostname: string;
|
||||||
|
|||||||
@@ -330,55 +330,6 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get average block health for all blocks for a single pool
|
|
||||||
*/
|
|
||||||
public async $getAvgBlockHealthPerPoolId(poolId: number): Promise<number> {
|
|
||||||
const params: any[] = [];
|
|
||||||
const query = `
|
|
||||||
SELECT AVG(blocks_audits.match_rate) AS avg_match_rate
|
|
||||||
FROM blocks
|
|
||||||
JOIN blocks_audits ON blocks.height = blocks_audits.height
|
|
||||||
WHERE blocks.pool_id = ?
|
|
||||||
`;
|
|
||||||
params.push(poolId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [rows] = await DB.query(query, params);
|
|
||||||
if (!rows[0] || !rows[0].avg_match_rate) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return Math.round(rows[0].avg_match_rate * 100) / 100;
|
|
||||||
} catch (e) {
|
|
||||||
logger.err(`Cannot get average block health for pool id ${poolId}. Reason: ` + (e instanceof Error ? e.message : e));
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get average block health for all blocks for a single pool
|
|
||||||
*/
|
|
||||||
public async $getTotalRewardForPoolId(poolId: number): Promise<number> {
|
|
||||||
const params: any[] = [];
|
|
||||||
const query = `
|
|
||||||
SELECT sum(reward) as total_reward
|
|
||||||
FROM blocks
|
|
||||||
WHERE blocks.pool_id = ?
|
|
||||||
`;
|
|
||||||
params.push(poolId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [rows] = await DB.query(query, params);
|
|
||||||
if (!rows[0] || !rows[0].total_reward) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return rows[0].total_reward;
|
|
||||||
} catch (e) {
|
|
||||||
logger.err(`Cannot get total reward for pool id ${poolId}. Reason: ` + (e instanceof Error ? e.message : e));
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the oldest indexed block
|
* Get the oldest indexed block
|
||||||
*/
|
*/
|
||||||
@@ -797,7 +748,6 @@ class BlocksRepository {
|
|||||||
SELECT height
|
SELECT height
|
||||||
FROM compact_cpfp_clusters
|
FROM compact_cpfp_clusters
|
||||||
WHERE height <= ? AND height >= ?
|
WHERE height <= ? AND height >= ?
|
||||||
GROUP BY height
|
|
||||||
ORDER BY height DESC;
|
ORDER BY height DESC;
|
||||||
`, [currentBlockHeight, minHeight]);
|
`, [currentBlockHeight, minHeight]);
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ class DifficultyAdjustmentsRepository {
|
|||||||
await DB.query(query, params);
|
await DB.query(query, params);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||||
logger.debug(`Cannot save difficulty adjustment at block ${adjustment.height}, already indexed, ignoring`, logger.tags.mining);
|
logger.debug(`Cannot save difficulty adjustment at block ${adjustment.height}, already indexed, ignoring`);
|
||||||
} else {
|
} else {
|
||||||
logger.err(`Cannot save difficulty adjustment at block ${adjustment.height}. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
|
logger.err(`Cannot save difficulty adjustment at block ${adjustment.height}. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ class DifficultyAdjustmentsRepository {
|
|||||||
const [rows] = await DB.query(query);
|
const [rows] = await DB.query(query);
|
||||||
return rows as IndexedDifficultyAdjustment[];
|
return rows as IndexedDifficultyAdjustment[];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e), logger.tags.mining);
|
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ class DifficultyAdjustmentsRepository {
|
|||||||
const [rows] = await DB.query(query);
|
const [rows] = await DB.query(query);
|
||||||
return rows as IndexedDifficultyAdjustment[];
|
return rows as IndexedDifficultyAdjustment[];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e), logger.tags.mining);
|
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,27 +93,27 @@ class DifficultyAdjustmentsRepository {
|
|||||||
const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`);
|
const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`);
|
||||||
return rows.map(block => block.height);
|
return rows.map(block => block.height);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.err(`Cannot get difficulty adjustment block heights. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
|
logger.err(`Cannot get difficulty adjustment block heights. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $deleteAdjustementsFromHeight(height: number): Promise<void> {
|
public async $deleteAdjustementsFromHeight(height: number): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logger.info(`Delete newer difficulty adjustments from height ${height} from the database`, logger.tags.mining);
|
logger.info(`Delete newer difficulty adjustments from height ${height} from the database`);
|
||||||
await DB.query(`DELETE FROM difficulty_adjustments WHERE height >= ?`, [height]);
|
await DB.query(`DELETE FROM difficulty_adjustments WHERE height >= ?`, [height]);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.err(`Cannot delete difficulty adjustments from the database. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
|
logger.err(`Cannot delete difficulty adjustments from the database. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $deleteLastAdjustment(): Promise<void> {
|
public async $deleteLastAdjustment(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logger.info(`Delete last difficulty adjustment from the database`, logger.tags.mining);
|
logger.info(`Delete last difficulty adjustment from the database`);
|
||||||
await DB.query(`DELETE FROM difficulty_adjustments ORDER BY time LIMIT 1`);
|
await DB.query(`DELETE FROM difficulty_adjustments ORDER BY time LIMIT 1`);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.err(`Cannot delete last difficulty adjustment from the database. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
|
logger.err(`Cannot delete last difficulty adjustment from the database. Reason: ${e instanceof Error ? e.message : e}`);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class HashratesRepository {
|
|||||||
try {
|
try {
|
||||||
await DB.query(query);
|
await DB.query(query);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.err('Cannot save indexed hashrate into db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
logger.err('Cannot save indexed hashrate into db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ class HashratesRepository {
|
|||||||
const [rows]: any[] = await DB.query(query);
|
const [rows]: any[] = await DB.query(query);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ class HashratesRepository {
|
|||||||
const [rows]: any[] = await DB.query(query);
|
const [rows]: any[] = await DB.query(query);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
logger.err('Cannot fetch network hashrate history. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ class HashratesRepository {
|
|||||||
const [rows]: any[] = await DB.query(query);
|
const [rows]: any[] = await DB.query(query);
|
||||||
return rows.map(row => row.timestamp);
|
return rows.map(row => row.timestamp);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
logger.err('Cannot retreive indexed weekly hashrate timestamps. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,7 +128,7 @@ class HashratesRepository {
|
|||||||
const [rows]: any[] = await DB.query(query);
|
const [rows]: any[] = await DB.query(query);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot fetch weekly pools hashrate history. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
logger.err('Cannot fetch weekly pools hashrate history. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,7 +158,7 @@ class HashratesRepository {
|
|||||||
const [rows]: any[] = await DB.query(query, [pool.id]);
|
const [rows]: any[] = await DB.query(query, [pool.id]);
|
||||||
boundaries = rows[0];
|
boundaries = rows[0];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot fetch hashrate start/end timestamps for this pool. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
logger.err('Cannot fetch hashrate start/end timestamps for this pool. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get hashrates entries between boundaries
|
// Get hashrates entries between boundaries
|
||||||
@@ -173,7 +173,7 @@ class HashratesRepository {
|
|||||||
const [rows]: any[] = await DB.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, pool.id]);
|
const [rows]: any[] = await DB.query(query, [boundaries.firstTimestamp, boundaries.lastTimestamp, pool.id]);
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot fetch pool hashrate history for this pool. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
logger.err('Cannot fetch pool hashrate history for this pool. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,7 +192,7 @@ class HashratesRepository {
|
|||||||
}
|
}
|
||||||
return rows[0]['number'];
|
return rows[0]['number'];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot retrieve last indexing run for ${key}. Reason: ` + (e instanceof Error ? e.message : e), logger.tags.mining);
|
logger.err(`Cannot retrieve last indexing run for ${key}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,7 +201,7 @@ class HashratesRepository {
|
|||||||
* Delete most recent data points for re-indexing
|
* Delete most recent data points for re-indexing
|
||||||
*/
|
*/
|
||||||
public async $deleteLastEntries() {
|
public async $deleteLastEntries() {
|
||||||
logger.info(`Delete latest hashrates data points from the database`, logger.tags.mining);
|
logger.info(`Delete latest hashrates data points from the database`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [rows]: any[] = await DB.query(`SELECT MAX(hashrate_timestamp) as timestamp FROM hashrates GROUP BY type`);
|
const [rows]: any[] = await DB.query(`SELECT MAX(hashrate_timestamp) as timestamp FROM hashrates GROUP BY type`);
|
||||||
@@ -212,7 +212,7 @@ class HashratesRepository {
|
|||||||
mining.lastHashrateIndexingDate = null;
|
mining.lastHashrateIndexingDate = null;
|
||||||
mining.lastWeeklyHashrateIndexingDate = null;
|
mining.lastWeeklyHashrateIndexingDate = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ class HashratesRepository {
|
|||||||
mining.lastHashrateIndexingDate = null;
|
mining.lastHashrateIndexingDate = null;
|
||||||
mining.lastWeeklyHashrateIndexingDate = null;
|
mining.lastWeeklyHashrateIndexingDate = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
logger.err('Cannot delete latest hashrates data points. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
|
import { IConversionRates } from '../mempool.interfaces';
|
||||||
import priceUpdater from '../tasks/price-updater';
|
import priceUpdater from '../tasks/price-updater';
|
||||||
|
|
||||||
export interface ApiPrice {
|
export interface ApiPrice {
|
||||||
@@ -12,16 +13,6 @@ export interface ApiPrice {
|
|||||||
AUD: number,
|
AUD: number,
|
||||||
JPY: number,
|
JPY: number,
|
||||||
}
|
}
|
||||||
const ApiPriceFields = `
|
|
||||||
UNIX_TIMESTAMP(time) as time,
|
|
||||||
USD,
|
|
||||||
EUR,
|
|
||||||
GBP,
|
|
||||||
CAD,
|
|
||||||
CHF,
|
|
||||||
AUD,
|
|
||||||
JPY
|
|
||||||
`;
|
|
||||||
|
|
||||||
export interface ExchangeRates {
|
export interface ExchangeRates {
|
||||||
USDEUR: number,
|
USDEUR: number,
|
||||||
@@ -48,7 +39,7 @@ export const MAX_PRICES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class PricesRepository {
|
class PricesRepository {
|
||||||
public async $savePrices(time: number, prices: ApiPrice): Promise<void> {
|
public async $savePrices(time: number, prices: IConversionRates): Promise<void> {
|
||||||
if (prices.USD === -1) {
|
if (prices.USD === -1) {
|
||||||
// Some historical price entries have no USD prices, so we just ignore them to avoid future UX issues
|
// Some historical price entries have no USD prices, so we just ignore them to avoid future UX issues
|
||||||
// As of today there are only 4 (on 2013-09-05, 2013-0909, 2013-09-12 and 2013-09-26) so that's fine
|
// As of today there are only 4 (on 2013-09-05, 2013-0909, 2013-09-12 and 2013-09-26) so that's fine
|
||||||
@@ -69,115 +60,77 @@ class PricesRepository {
|
|||||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`,
|
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`,
|
||||||
[time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY]
|
[time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY]
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getOldestPriceTime(): Promise<number> {
|
public async $getOldestPriceTime(): Promise<number> {
|
||||||
const [oldestRow] = await DB.query(`
|
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time LIMIT 1`);
|
||||||
SELECT UNIX_TIMESTAMP(time) AS time
|
|
||||||
FROM prices
|
|
||||||
ORDER BY time
|
|
||||||
LIMIT 1
|
|
||||||
`);
|
|
||||||
return oldestRow[0] ? oldestRow[0].time : 0;
|
return oldestRow[0] ? oldestRow[0].time : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getLatestPriceId(): Promise<number | null> {
|
public async $getLatestPriceId(): Promise<number | null> {
|
||||||
const [oldestRow] = await DB.query(`
|
const [oldestRow] = await DB.query(`SELECT id from prices WHERE USD != 0 ORDER BY time DESC LIMIT 1`);
|
||||||
SELECT id
|
|
||||||
FROM prices
|
|
||||||
ORDER BY time DESC
|
|
||||||
LIMIT 1`
|
|
||||||
);
|
|
||||||
return oldestRow[0] ? oldestRow[0].id : null;
|
return oldestRow[0] ? oldestRow[0].id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getLatestPriceTime(): Promise<number> {
|
public async $getLatestPriceTime(): Promise<number> {
|
||||||
const [oldestRow] = await DB.query(`
|
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time DESC LIMIT 1`);
|
||||||
SELECT UNIX_TIMESTAMP(time) AS time
|
|
||||||
FROM prices
|
|
||||||
ORDER BY time DESC
|
|
||||||
LIMIT 1`
|
|
||||||
);
|
|
||||||
return oldestRow[0] ? oldestRow[0].time : 0;
|
return oldestRow[0] ? oldestRow[0].time : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getPricesTimes(): Promise<number[]> {
|
public async $getPricesTimes(): Promise<number[]> {
|
||||||
const [times] = await DB.query(`
|
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time`);
|
||||||
SELECT UNIX_TIMESTAMP(time) AS time
|
|
||||||
FROM prices
|
|
||||||
WHERE USD != -1
|
|
||||||
ORDER BY time
|
|
||||||
`);
|
|
||||||
if (!Array.isArray(times)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return times.map(time => time.time);
|
return times.map(time => time.time);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getPricesTimesAndId(): Promise<{time: number, id: number, USD: number}[]> {
|
public async $getPricesTimesAndId(): Promise<number[]> {
|
||||||
const [times] = await DB.query(`
|
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time, id, USD from prices ORDER BY time`);
|
||||||
SELECT
|
return times;
|
||||||
UNIX_TIMESTAMP(time) AS time,
|
|
||||||
id,
|
|
||||||
USD
|
|
||||||
FROM prices
|
|
||||||
ORDER BY time
|
|
||||||
`);
|
|
||||||
return times as {time: number, id: number, USD: number}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getLatestConversionRates(): Promise<ApiPrice> {
|
public async $getLatestConversionRates(): Promise<any> {
|
||||||
const [rates] = await DB.query(`
|
const [rates]: any[] = await DB.query(`
|
||||||
SELECT ${ApiPriceFields}
|
SELECT USD, EUR, GBP, CAD, CHF, AUD, JPY
|
||||||
FROM prices
|
FROM prices
|
||||||
ORDER BY time DESC
|
ORDER BY time DESC
|
||||||
LIMIT 1`
|
LIMIT 1`
|
||||||
);
|
);
|
||||||
|
if (!rates || rates.length === 0) {
|
||||||
if (!Array.isArray(rates) || rates.length === 0) {
|
|
||||||
return priceUpdater.getEmptyPricesObj();
|
return priceUpdater.getEmptyPricesObj();
|
||||||
}
|
}
|
||||||
return rates[0] as ApiPrice;
|
return rates[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> {
|
public async $getNearestHistoricalPrice(timestamp: number | undefined): Promise<Conversion | null> {
|
||||||
try {
|
try {
|
||||||
const [rates] = await DB.query(`
|
const [rates]: any[] = await DB.query(`
|
||||||
SELECT ${ApiPriceFields}
|
SELECT *, UNIX_TIMESTAMP(time) AS time
|
||||||
FROM prices
|
FROM prices
|
||||||
WHERE UNIX_TIMESTAMP(time) < ?
|
WHERE UNIX_TIMESTAMP(time) < ?
|
||||||
ORDER BY time DESC
|
ORDER BY time DESC
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
[timestamp]
|
[timestamp]
|
||||||
);
|
);
|
||||||
if (!Array.isArray(rates)) {
|
if (!rates) {
|
||||||
throw Error(`Cannot get single historical price from the database`);
|
throw Error(`Cannot get single historical price from the database`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute fiat exchange rates
|
// Compute fiat exchange rates
|
||||||
let latestPrice = rates[0] as ApiPrice;
|
const latestPrice = await this.$getLatestConversionRates();
|
||||||
if (latestPrice.USD === -1) {
|
|
||||||
latestPrice = priceUpdater.getEmptyPricesObj();
|
|
||||||
}
|
|
||||||
|
|
||||||
const computeFx = (usd: number, other: number): number =>
|
|
||||||
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
|
|
||||||
|
|
||||||
const exchangeRates: ExchangeRates = {
|
const exchangeRates: ExchangeRates = {
|
||||||
USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
|
USDEUR: Math.round(latestPrice.EUR / latestPrice.USD * 100) / 100,
|
||||||
USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
|
USDGBP: Math.round(latestPrice.GBP / latestPrice.USD * 100) / 100,
|
||||||
USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
|
USDCAD: Math.round(latestPrice.CAD / latestPrice.USD * 100) / 100,
|
||||||
USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
|
USDCHF: Math.round(latestPrice.CHF / latestPrice.USD * 100) / 100,
|
||||||
USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
|
USDAUD: Math.round(latestPrice.AUD / latestPrice.USD * 100) / 100,
|
||||||
USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
|
USDJPY: Math.round(latestPrice.JPY / latestPrice.USD * 100) / 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
prices: rates as ApiPrice[],
|
prices: rates,
|
||||||
exchangeRates: exchangeRates
|
exchangeRates: exchangeRates
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -188,35 +141,28 @@ class PricesRepository {
|
|||||||
|
|
||||||
public async $getHistoricalPrices(): Promise<Conversion | null> {
|
public async $getHistoricalPrices(): Promise<Conversion | null> {
|
||||||
try {
|
try {
|
||||||
const [rates] = await DB.query(`
|
const [rates]: any[] = await DB.query(`
|
||||||
SELECT ${ApiPriceFields}
|
SELECT *, UNIX_TIMESTAMP(time) AS time
|
||||||
FROM prices
|
FROM prices
|
||||||
ORDER BY time DESC
|
ORDER BY time DESC
|
||||||
`);
|
`);
|
||||||
if (!Array.isArray(rates)) {
|
if (!rates) {
|
||||||
throw Error(`Cannot get average historical price from the database`);
|
throw Error(`Cannot get average historical price from the database`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute fiat exchange rates
|
// Compute fiat exchange rates
|
||||||
let latestPrice = rates[0] as ApiPrice;
|
const latestPrice: ApiPrice = rates[0];
|
||||||
if (latestPrice.USD === -1) {
|
|
||||||
latestPrice = priceUpdater.getEmptyPricesObj();
|
|
||||||
}
|
|
||||||
|
|
||||||
const computeFx = (usd: number, other: number): number =>
|
|
||||||
Math.round(Math.max(other, 0) / Math.max(usd, 1) * 100) / 100;
|
|
||||||
|
|
||||||
const exchangeRates: ExchangeRates = {
|
const exchangeRates: ExchangeRates = {
|
||||||
USDEUR: computeFx(latestPrice.USD, latestPrice.EUR),
|
USDEUR: Math.round(latestPrice.EUR / latestPrice.USD * 100) / 100,
|
||||||
USDGBP: computeFx(latestPrice.USD, latestPrice.GBP),
|
USDGBP: Math.round(latestPrice.GBP / latestPrice.USD * 100) / 100,
|
||||||
USDCAD: computeFx(latestPrice.USD, latestPrice.CAD),
|
USDCAD: Math.round(latestPrice.CAD / latestPrice.USD * 100) / 100,
|
||||||
USDCHF: computeFx(latestPrice.USD, latestPrice.CHF),
|
USDCHF: Math.round(latestPrice.CHF / latestPrice.USD * 100) / 100,
|
||||||
USDAUD: computeFx(latestPrice.USD, latestPrice.AUD),
|
USDAUD: Math.round(latestPrice.AUD / latestPrice.USD * 100) / 100,
|
||||||
USDJPY: computeFx(latestPrice.USD, latestPrice.JPY),
|
USDJPY: Math.round(latestPrice.JPY / latestPrice.USD * 100) / 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
prices: rates as ApiPrice[],
|
prices: rates,
|
||||||
exchangeRates: exchangeRates
|
exchangeRates: exchangeRates
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -411,7 +411,7 @@ class LightningStatsImporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (totalProcessed > 0) {
|
if (totalProcessed > 0) {
|
||||||
logger.info(`Lightning network stats historical import completed`, logger.tags.ln);
|
logger.notice(`Lightning network stats historical import completed`, logger.tags.ln);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln);
|
logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln);
|
||||||
|
|||||||
@@ -12,14 +12,12 @@ import * as https from 'https';
|
|||||||
*/
|
*/
|
||||||
class PoolsUpdater {
|
class PoolsUpdater {
|
||||||
lastRun: number = 0;
|
lastRun: number = 0;
|
||||||
currentSha: string | null = null;
|
currentSha: string | undefined = undefined;
|
||||||
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
|
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
|
||||||
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
|
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
|
||||||
|
|
||||||
public async updatePoolsJson(): Promise<void> {
|
public async updatePoolsJson(): Promise<void> {
|
||||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) {
|
||||||
config.MEMPOOL.ENABLED === false
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +33,7 @@ class PoolsUpdater {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const githubSha = await this.fetchPoolsSha(); // Fetch pools-v2.json sha from github
|
const githubSha = await this.fetchPoolsSha(); // Fetch pools-v2.json sha from github
|
||||||
if (githubSha === null) {
|
if (githubSha === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,12 +42,12 @@ class PoolsUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
|
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
|
||||||
if (this.currentSha !== null && this.currentSha === githubSha) {
|
if (this.currentSha !== undefined && this.currentSha === githubSha) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// See backend README for more details about the mining pools update process
|
// See backend README for more details about the mining pools update process
|
||||||
if (this.currentSha !== null && // If we don't have any mining pool, download it at least once
|
if (this.currentSha !== undefined && // If we don't have any mining pool, download it at least once
|
||||||
config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING !== true && // Automatic pools update is disabled
|
config.MEMPOOL.AUTOMATIC_BLOCK_REINDEXING !== true && // Automatic pools update is disabled
|
||||||
!process.env.npm_config_update_pools // We're not manually updating mining pool
|
!process.env.npm_config_update_pools // We're not manually updating mining pool
|
||||||
) {
|
) {
|
||||||
@@ -59,7 +57,7 @@ class PoolsUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
|
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
|
||||||
if (this.currentSha === null) {
|
if (this.currentSha === undefined) {
|
||||||
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining);
|
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`pools-v2.json is outdated, fetch latest from ${this.poolsUrl} over ${network}`, logger.tags.mining);
|
logger.warn(`pools-v2.json is outdated, fetch latest from ${this.poolsUrl} over ${network}`, logger.tags.mining);
|
||||||
@@ -84,7 +82,7 @@ class PoolsUpdater {
|
|||||||
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
|
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
|
||||||
await DB.query('ROLLBACK;');
|
await DB.query('ROLLBACK;');
|
||||||
}
|
}
|
||||||
logger.info('PoolsUpdater completed');
|
logger.notice('PoolsUpdater completed');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
|
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
|
||||||
@@ -110,20 +108,20 @@ class PoolsUpdater {
|
|||||||
/**
|
/**
|
||||||
* Fetch our latest pools-v2.json sha from the db
|
* Fetch our latest pools-v2.json sha from the db
|
||||||
*/
|
*/
|
||||||
private async getShaFromDb(): Promise<string | null> {
|
private async getShaFromDb(): Promise<string | undefined> {
|
||||||
try {
|
try {
|
||||||
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
|
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
|
||||||
return (rows.length > 0 ? rows[0].string : null);
|
return (rows.length > 0 ? rows[0].string : undefined);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch our latest pools-v2.json sha from github
|
* Fetch our latest pools-v2.json sha from github
|
||||||
*/
|
*/
|
||||||
private async fetchPoolsSha(): Promise<string | null> {
|
private async fetchPoolsSha(): Promise<string | undefined> {
|
||||||
const response = await this.query(this.treeUrl);
|
const response = await this.query(this.treeUrl);
|
||||||
|
|
||||||
if (response !== undefined) {
|
if (response !== undefined) {
|
||||||
@@ -135,7 +133,7 @@ class PoolsUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining);
|
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining);
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ class BitfinexApi implements PriceFeed {
|
|||||||
public url: string = 'https://api.bitfinex.com/v1/pubticker/BTC';
|
public url: string = 'https://api.bitfinex.com/v1/pubticker/BTC';
|
||||||
public urlHist: string = 'https://api-pub.bitfinex.com/v2/candles/trade:{GRANULARITY}:tBTC{CURRENCY}/hist';
|
public urlHist: string = 'https://api-pub.bitfinex.com/v2/candles/trade:{GRANULARITY}:tBTC{CURRENCY}/hist';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
public async $fetchPrice(currency): Promise<number> {
|
public async $fetchPrice(currency): Promise<number> {
|
||||||
const response = await query(this.url + currency);
|
const response = await query(this.url + currency);
|
||||||
if (response && response['last_price']) {
|
if (response && response['last_price']) {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class KrakenApi implements PriceFeed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(priceHistory).length > 0) {
|
if (Object.keys(priceHistory).length > 0) {
|
||||||
logger.info(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`, logger.tags.mining);
|
logger.notice(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import * as fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import PricesRepository, { ApiPrice, MAX_PRICES } from '../repositories/PricesRepository';
|
import { IConversionRates } from '../mempool.interfaces';
|
||||||
|
import PricesRepository, { MAX_PRICES } from '../repositories/PricesRepository';
|
||||||
import BitfinexApi from './price-feeds/bitfinex-api';
|
import BitfinexApi from './price-feeds/bitfinex-api';
|
||||||
import BitflyerApi from './price-feeds/bitflyer-api';
|
import BitflyerApi from './price-feeds/bitflyer-api';
|
||||||
import CoinbaseApi from './price-feeds/coinbase-api';
|
import CoinbaseApi from './price-feeds/coinbase-api';
|
||||||
@@ -20,18 +21,18 @@ export interface PriceFeed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PriceHistory {
|
export interface PriceHistory {
|
||||||
[timestamp: number]: ApiPrice;
|
[timestamp: number]: IConversionRates;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PriceUpdater {
|
class PriceUpdater {
|
||||||
public historyInserted = false;
|
public historyInserted = false;
|
||||||
private lastRun = 0;
|
lastRun = 0;
|
||||||
private lastHistoricalRun = 0;
|
lastHistoricalRun = 0;
|
||||||
private running = false;
|
running = false;
|
||||||
private feeds: PriceFeed[] = [];
|
feeds: PriceFeed[] = [];
|
||||||
private currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
|
currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
|
||||||
private latestPrices: ApiPrice;
|
latestPrices: IConversionRates;
|
||||||
private ratesChangedCallback: ((rates: ApiPrice) => void) | undefined;
|
private ratesChangedCallback: ((rates: IConversionRates) => void) | undefined;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.latestPrices = this.getEmptyPricesObj();
|
this.latestPrices = this.getEmptyPricesObj();
|
||||||
@@ -43,13 +44,8 @@ class PriceUpdater {
|
|||||||
this.feeds.push(new GeminiApi());
|
this.feeds.push(new GeminiApi());
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLatestPrices(): ApiPrice {
|
public getEmptyPricesObj(): IConversionRates {
|
||||||
return this.latestPrices;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getEmptyPricesObj(): ApiPrice {
|
|
||||||
return {
|
return {
|
||||||
time: 0,
|
|
||||||
USD: -1,
|
USD: -1,
|
||||||
EUR: -1,
|
EUR: -1,
|
||||||
GBP: -1,
|
GBP: -1,
|
||||||
@@ -60,7 +56,7 @@ class PriceUpdater {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public setRatesChangedCallback(fn: (rates: ApiPrice) => void): void {
|
public setRatesChangedCallback(fn: (rates: IConversionRates) => void) {
|
||||||
this.ratesChangedCallback = fn;
|
this.ratesChangedCallback = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,11 +69,6 @@ class PriceUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $run(): Promise<void> {
|
public async $run(): Promise<void> {
|
||||||
if (config.MEMPOOL.NETWORK === 'signet' || config.MEMPOOL.NETWORK === 'testnet') {
|
|
||||||
// Coins have no value on testnet/signet, so we want to always show 0
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.running === true) {
|
if (this.running === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -93,7 +84,7 @@ class PriceUpdater {
|
|||||||
if (this.historyInserted === false && config.DATABASE.ENABLED === true) {
|
if (this.historyInserted === false && config.DATABASE.ENABLED === true) {
|
||||||
await this.$insertHistoricalPrices();
|
await this.$insertHistoricalPrices();
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
|
logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,10 +156,6 @@ class PriceUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.lastRun = new Date().getTime() / 1000;
|
this.lastRun = new Date().getTime() / 1000;
|
||||||
|
|
||||||
if (this.latestPrices.USD === -1) {
|
|
||||||
this.latestPrices = await PricesRepository.$getLatestConversionRates();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -237,7 +224,7 @@ class PriceUpdater {
|
|||||||
|
|
||||||
// Group them by timestamp and currency, for example
|
// Group them by timestamp and currency, for example
|
||||||
// grouped[123456789]['USD'] = [1, 2, 3, 4];
|
// grouped[123456789]['USD'] = [1, 2, 3, 4];
|
||||||
const grouped = {};
|
const grouped: any = {};
|
||||||
for (const historicalEntry of historicalPrices) {
|
for (const historicalEntry of historicalPrices) {
|
||||||
for (const time in historicalEntry) {
|
for (const time in historicalEntry) {
|
||||||
if (existingPriceTimes.includes(parseInt(time, 10))) {
|
if (existingPriceTimes.includes(parseInt(time, 10))) {
|
||||||
@@ -262,7 +249,7 @@ class PriceUpdater {
|
|||||||
// Average prices and insert everything into the db
|
// Average prices and insert everything into the db
|
||||||
let totalInserted = 0;
|
let totalInserted = 0;
|
||||||
for (const time in grouped) {
|
for (const time in grouped) {
|
||||||
const prices: ApiPrice = this.getEmptyPricesObj();
|
const prices: IConversionRates = this.getEmptyPricesObj();
|
||||||
for (const currency in grouped[time]) {
|
for (const currency in grouped[time]) {
|
||||||
if (grouped[time][currency].length === 0) {
|
if (grouped[time][currency].length === 0) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
const byteUnits = ['B', 'kB', 'MB', 'GB', 'TB'];
|
|
||||||
|
|
||||||
export function getBytesUnit(bytes: number): string {
|
|
||||||
if (isNaN(bytes) || !isFinite(bytes)) {
|
|
||||||
return 'B';
|
|
||||||
}
|
|
||||||
|
|
||||||
let unitIndex = 0;
|
|
||||||
while (unitIndex < byteUnits.length && bytes > 1024) {
|
|
||||||
unitIndex++;
|
|
||||||
bytes /= 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
return byteUnits[unitIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatBytes(bytes: number, toUnit: string, skipUnit = false): string {
|
|
||||||
if (isNaN(bytes) || !isFinite(bytes)) {
|
|
||||||
return `${bytes}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let unitIndex = 0;
|
|
||||||
while (unitIndex < byteUnits.length && (toUnit && byteUnits[unitIndex] !== toUnit || (!toUnit && bytes > 1024))) {
|
|
||||||
unitIndex++;
|
|
||||||
bytes /= 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${bytes.toFixed(2)}${skipUnit ? '' : ' ' + byteUnits[unitIndex]}`;
|
|
||||||
}
|
|
||||||
@@ -112,7 +112,6 @@ Below we list all settings from `mempool-config.json` and the corresponding over
|
|||||||
"ADVANCED_GBT_MEMPOOL": false,
|
"ADVANCED_GBT_MEMPOOL": false,
|
||||||
"CPFP_INDEXING": false,
|
"CPFP_INDEXING": false,
|
||||||
"MAX_BLOCKS_BULK_QUERY": 0,
|
"MAX_BLOCKS_BULK_QUERY": 0,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 6
|
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -144,7 +143,6 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
MEMPOOL_ADVANCED_GBT_MEMPOOL: ""
|
MEMPOOL_ADVANCED_GBT_MEMPOOL: ""
|
||||||
MEMPOOL_CPFP_INDEXING: ""
|
MEMPOOL_CPFP_INDEXING: ""
|
||||||
MAX_BLOCKS_BULK_QUERY: ""
|
MAX_BLOCKS_BULK_QUERY: ""
|
||||||
DISK_CACHE_BLOCK_INTERVAL: ""
|
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -434,26 +432,3 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
CLIGHTNING_SOCKET: ""
|
CLIGHTNING_SOCKET: ""
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
`mempool-config.json`:
|
|
||||||
```json
|
|
||||||
"MAXMIND": {
|
|
||||||
"ENABLED": true,
|
|
||||||
"GEOLITE2_CITY": "/usr/local/share/GeoIP/GeoLite2-City.mmdb",
|
|
||||||
"GEOLITE2_ASN": "/usr/local/share/GeoIP/GeoLite2-ASN.mmdb",
|
|
||||||
"GEOIP2_ISP": "/usr/local/share/GeoIP/GeoIP2-ISP.mmdb"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Corresponding `docker-compose.yml` overrides:
|
|
||||||
```yaml
|
|
||||||
api:
|
|
||||||
environment:
|
|
||||||
MAXMIND_ENABLED: true,
|
|
||||||
MAXMIND_GEOLITE2_CITY: "/backend/GeoIP/GeoLite2-City.mmdb",
|
|
||||||
MAXMIND_GEOLITE2_ASN": "/backend/GeoIP/GeoLite2-ASN.mmdb",
|
|
||||||
MAXMIND_GEOIP2_ISP": "/backend/GeoIP/GeoIP2-ISP.mmdb"
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ WORKDIR /backend
|
|||||||
|
|
||||||
RUN chown 1000:1000 ./
|
RUN chown 1000:1000 ./
|
||||||
COPY --from=builder --chown=1000:1000 /build/package ./package/
|
COPY --from=builder --chown=1000:1000 /build/package ./package/
|
||||||
COPY --from=builder --chown=1000:1000 /build/GeoIP ./GeoIP/
|
|
||||||
COPY --from=builder --chown=1000:1000 /build/mempool-config.json /build/start.sh /build/wait-for-it.sh ./
|
COPY --from=builder --chown=1000:1000 /build/mempool-config.json /build/start.sh /build/wait-for-it.sh ./
|
||||||
|
|
||||||
USER 1000
|
USER 1000
|
||||||
|
|||||||
@@ -26,8 +26,7 @@
|
|||||||
"ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__,
|
"ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__,
|
||||||
"ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__,
|
"ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__,
|
||||||
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
|
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
|
||||||
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL_MAX_BLOCKS_BULK_QUERY__,
|
"MAX_BLOCKS_BULK_QUERY": __MEMPOOL__MAX_BLOCKS_BULK_QUERY__
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": __DISK_CACHE_BLOCK_INTERVAL__
|
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "__CORE_RPC_HOST__",
|
"HOST": "__CORE_RPC_HOST__",
|
||||||
@@ -108,11 +107,5 @@
|
|||||||
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
|
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
|
||||||
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
|
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
|
||||||
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
|
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
|
||||||
},
|
|
||||||
"MAXMIND": {
|
|
||||||
"ENABLED": __MAXMIND_ENABLED__,
|
|
||||||
"GEOLITE2_CITY": "__MAXMIND_GEOLITE2_CITY__",
|
|
||||||
"GEOLITE2_ASN": "__MAXMIND_GEOLITE2_ASN__",
|
|
||||||
"GEOIP2_ISP": "__MAXMIND_GEOIP2_ISP__"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ __MEMPOOL_EXTERNAL_MAX_RETRY__=${MEMPOOL_EXTERNAL_MAX_RETRY:=1}
|
|||||||
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
|
__MEMPOOL_EXTERNAL_RETRY_INTERVAL__=${MEMPOOL_EXTERNAL_RETRY_INTERVAL:=0}
|
||||||
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
|
__MEMPOOL_USER_AGENT__=${MEMPOOL_USER_AGENT:=mempool}
|
||||||
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
|
__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
|
||||||
|
__MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=false}
|
||||||
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
|
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
|
||||||
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
|
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
|
||||||
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
|
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
|
||||||
@@ -30,7 +31,6 @@ __MEMPOOL_ADVANCED_GBT_AUDIT__=${MEMPOOL_ADVANCED_GBT_AUDIT:=false}
|
|||||||
__MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false}
|
__MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false}
|
||||||
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
||||||
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
|
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
|
||||||
__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__=${MEMPOOL_DISK_CACHE_BLOCK_INTERVAL:=6}
|
|
||||||
|
|
||||||
# CORE_RPC
|
# CORE_RPC
|
||||||
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
|
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
|
||||||
@@ -112,13 +112,6 @@ __LND_REST_API_URL__=${LND_REST_API_URL:="https://localhost:8080"}
|
|||||||
# CLN
|
# CLN
|
||||||
__CLIGHTNING_SOCKET__=${CLIGHTNING_SOCKET:=""}
|
__CLIGHTNING_SOCKET__=${CLIGHTNING_SOCKET:=""}
|
||||||
|
|
||||||
# MAXMIND
|
|
||||||
__MAXMIND_ENABLED__=${MAXMIND_ENABLED:=true}
|
|
||||||
__MAXMIND_GEOLITE2_CITY__=${MAXMIND_GEOLITE2_CITY:="/backend/GeoIP/GeoLite2-City.mmdb"}
|
|
||||||
__MAXMIND_GEOLITE2_ASN__=${MAXMIND_GEOLITE2_ASN:="/backend/GeoIP/GeoLite2-ASN.mmdb"}
|
|
||||||
__MAXMIND_GEOIP2_ISP__=${MAXMIND_GEOIP2_ISP:=""}
|
|
||||||
|
|
||||||
|
|
||||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
|
||||||
|
|
||||||
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
|
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
|
||||||
@@ -142,6 +135,7 @@ sed -i "s!__MEMPOOL_EXTERNAL_MAX_RETRY__!${__MEMPOOL_EXTERNAL_MAX_RETRY__}!g" me
|
|||||||
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_EXTERNAL_RETRY_INTERVAL__!${__MEMPOOL_EXTERNAL_RETRY_INTERVAL__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_USER_AGENT__!${__MEMPOOL_USER_AGENT__}!g" mempool-config.json
|
||||||
sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json
|
sed -i "s/__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__/${__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__}/g" mempool-config.json
|
||||||
|
sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT__}/g" mempool-config.json
|
||||||
sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json
|
sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
|
||||||
@@ -150,7 +144,6 @@ sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g
|
|||||||
sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_MAX_BLOCKS_BULK_QUERY__!${__MEMPOOL_MAX_BLOCKS_BULK_QUERY__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__!${__MEMPOOL_DISK_CACHE_BLOCK_INTERVAL__}!g" mempool-config.json
|
|
||||||
|
|
||||||
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
|
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
|
||||||
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json
|
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json
|
||||||
@@ -222,11 +215,4 @@ sed -i "s!__LND_REST_API_URL__!${__LND_REST_API_URL__}!g" mempool-config.json
|
|||||||
# CLN
|
# CLN
|
||||||
sed -i "s!__CLIGHTNING_SOCKET__!${__CLIGHTNING_SOCKET__}!g" mempool-config.json
|
sed -i "s!__CLIGHTNING_SOCKET__!${__CLIGHTNING_SOCKET__}!g" mempool-config.json
|
||||||
|
|
||||||
# MAXMIND
|
|
||||||
sed -i "s!__MAXMIND_ENABLED__!${__MAXMIND_ENABLED__}!g" mempool-config.json
|
|
||||||
sed -i "s!__MAXMIND_GEOLITE2_CITY__!${__MAXMIND_GEOLITE2_CITY__}!g" mempool-config.json
|
|
||||||
sed -i "s!__MAXMIND_GEOLITE2_ASN__!${__MAXMIND_GEOLITE2_ASN__}!g" mempool-config.json
|
|
||||||
sed -i "s!__MAXMIND_GEOIP2_ISP__!${__MAXMIND_GEOIP2_ISP__}!g" mempool-config.json
|
|
||||||
|
|
||||||
|
|
||||||
node /backend/package/index.js
|
node /backend/package/index.js
|
||||||
|
|||||||
@@ -3,11 +3,6 @@
|
|||||||
#backend
|
#backend
|
||||||
cp ./docker/backend/* ./backend/
|
cp ./docker/backend/* ./backend/
|
||||||
|
|
||||||
#geoip-data
|
|
||||||
mkdir -p ./backend/GeoIP/
|
|
||||||
wget -O ./backend/GeoIP/GeoLite2-City.mmdb https://raw.githubusercontent.com/mempool/geoip-data/master/GeoLite2-City.mmdb
|
|
||||||
wget -O ./backend/GeoIP/GeoLite2-ASN.mmdb https://raw.githubusercontent.com/mempool/geoip-data/master/GeoLite2-ASN.mmdb
|
|
||||||
|
|
||||||
#frontend
|
#frontend
|
||||||
localhostIP="127.0.0.1"
|
localhostIP="127.0.0.1"
|
||||||
cp ./docker/frontend/* ./frontend
|
cp ./docker/frontend/* ./frontend
|
||||||
|
|||||||
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@@ -54,8 +54,7 @@ src/resources/assets-testnet.json
|
|||||||
src/resources/assets-testnet.minimal.json
|
src/resources/assets-testnet.minimal.json
|
||||||
src/resources/pools.json
|
src/resources/pools.json
|
||||||
src/resources/mining-pools/*
|
src/resources/mining-pools/*
|
||||||
src/resources/**/*.mp4
|
src/resources/*.mp4
|
||||||
src/resources/**/*.vtt
|
|
||||||
|
|
||||||
# environment config
|
# environment config
|
||||||
mempool-frontend-config.json
|
mempool-frontend-config.json
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ https://www.transifex.com/mempool/mempool/dashboard/
|
|||||||
* Spanish @maxhodler @bisqes
|
* Spanish @maxhodler @bisqes
|
||||||
* Persian @techmix
|
* Persian @techmix
|
||||||
* French @Bayernatoor
|
* French @Bayernatoor
|
||||||
* Korean @kcalvinalvinn @sogoagain
|
* Korean @kcalvinalvinn
|
||||||
* Italian @HodlBits
|
* Italian @HodlBits
|
||||||
* Hebrew @rapidlab309
|
* Hebrew @rapidlab309
|
||||||
* Georgian @wyd_idk
|
* Georgian @wyd_idk
|
||||||
|
|||||||
@@ -38,10 +38,6 @@
|
|||||||
"translation": "src/locale/messages.de.xlf",
|
"translation": "src/locale/messages.de.xlf",
|
||||||
"baseHref": "/de/"
|
"baseHref": "/de/"
|
||||||
},
|
},
|
||||||
"da": {
|
|
||||||
"translation": "src/locale/messages.da.xlf",
|
|
||||||
"baseHref": "/da/"
|
|
||||||
},
|
|
||||||
"es": {
|
"es": {
|
||||||
"translation": "src/locale/messages.es.xlf",
|
"translation": "src/locale/messages.es.xlf",
|
||||||
"baseHref": "/es/"
|
"baseHref": "/es/"
|
||||||
|
|||||||
@@ -87,9 +87,9 @@ export const languages: Language[] = [
|
|||||||
{ code: 'ar', name: 'العربية' }, // Arabic
|
{ code: 'ar', name: 'العربية' }, // Arabic
|
||||||
// { code: 'bg', name: 'Български' }, // Bulgarian
|
// { code: 'bg', name: 'Български' }, // Bulgarian
|
||||||
// { code: 'bs', name: 'Bosanski' }, // Bosnian
|
// { code: 'bs', name: 'Bosanski' }, // Bosnian
|
||||||
// { code: 'ca', name: 'Català' }, // Catalan
|
{ code: 'ca', name: 'Català' }, // Catalan
|
||||||
{ code: 'cs', name: 'Čeština' }, // Czech
|
{ code: 'cs', name: 'Čeština' }, // Czech
|
||||||
{ code: 'da', name: 'Dansk' }, // Danish
|
// { code: 'da', name: 'Dansk' }, // Danish
|
||||||
{ code: 'de', name: 'Deutsch' }, // German
|
{ code: 'de', name: 'Deutsch' }, // German
|
||||||
// { code: 'et', name: 'Eesti' }, // Estonian
|
// { code: 'et', name: 'Eesti' }, // Estonian
|
||||||
// { code: 'el', name: 'Ελληνικά' }, // Greek
|
// { code: 'el', name: 'Ελληνικά' }, // Greek
|
||||||
@@ -136,90 +136,13 @@ export const languages: Language[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const specialBlocks = {
|
export const specialBlocks = {
|
||||||
'0': {
|
|
||||||
labelEvent: 'Genesis',
|
|
||||||
labelEventCompleted: 'The Genesis of Bitcoin',
|
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
},
|
|
||||||
'210000': {
|
|
||||||
labelEvent: 'Bitcoin\'s 1st Halving',
|
|
||||||
labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block',
|
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
},
|
|
||||||
'420000': {
|
|
||||||
labelEvent: 'Bitcoin\'s 2nd Halving',
|
|
||||||
labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block',
|
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
},
|
|
||||||
'630000': {
|
|
||||||
labelEvent: 'Bitcoin\'s 3rd Halving',
|
|
||||||
labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block',
|
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
},
|
|
||||||
'709632': {
|
'709632': {
|
||||||
labelEvent: 'Taproot 🌱 activation',
|
labelEvent: 'Taproot 🌱 activation',
|
||||||
labelEventCompleted: 'Taproot 🌱 has been activated!',
|
labelEventCompleted: 'Taproot 🌱 has been activated!',
|
||||||
networks: ['mainnet'],
|
|
||||||
},
|
},
|
||||||
'840000': {
|
'840000': {
|
||||||
labelEvent: 'Bitcoin\'s 4th Halving',
|
labelEvent: 'Halving 🥳',
|
||||||
labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
|
labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
},
|
|
||||||
'1050000': {
|
|
||||||
labelEvent: 'Bitcoin\'s 5th Halving',
|
|
||||||
labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block',
|
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
},
|
|
||||||
'1260000': {
|
|
||||||
labelEvent: 'Bitcoin\'s 6th Halving',
|
|
||||||
labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block',
|
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
},
|
|
||||||
'1470000': {
|
|
||||||
labelEvent: 'Bitcoin\'s 7th Halving',
|
|
||||||
labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block',
|
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
},
|
|
||||||
'1680000': {
|
|
||||||
labelEvent: 'Bitcoin\'s 8th Halving',
|
|
||||||
labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block',
|
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
},
|
|
||||||
'1890000': {
|
|
||||||
labelEvent: 'Bitcoin\'s 9th Halving',
|
|
||||||
labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block',
|
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
},
|
|
||||||
'2100000': {
|
|
||||||
labelEvent: 'Bitcoin\'s 10th Halving',
|
|
||||||
labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block',
|
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
},
|
|
||||||
'2310000': {
|
|
||||||
labelEvent: 'Bitcoin\'s 11th Halving',
|
|
||||||
labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block',
|
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
},
|
|
||||||
'2520000': {
|
|
||||||
labelEvent: 'Bitcoin\'s 12th Halving',
|
|
||||||
labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block',
|
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
},
|
|
||||||
'2730000': {
|
|
||||||
labelEvent: 'Bitcoin\'s 13th Halving',
|
|
||||||
labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block',
|
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
},
|
|
||||||
'2940000': {
|
|
||||||
labelEvent: 'Bitcoin\'s 14th Halving',
|
|
||||||
labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block',
|
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
},
|
|
||||||
'3150000': {
|
|
||||||
labelEvent: 'Bitcoin\'s 15th Halving',
|
|
||||||
labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block',
|
|
||||||
networks: ['mainnet', 'testnet'],
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<td>
|
<td>
|
||||||
‎{{ block.time | date:'yyyy-MM-dd HH:mm' }}
|
‎{{ block.time | date:'yyyy-MM-dd HH:mm' }}
|
||||||
<div class="lg-inline">
|
<div class="lg-inline">
|
||||||
<i class="symbol">(<app-time kind="since" [time]="block.time / 1000" [fastRender]="true"></app-time>)</i>
|
<i class="symbol">(<app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since>)</i>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<tbody *ngIf="blocks.value; else loadingTmpl">
|
<tbody *ngIf="blocks.value; else loadingTmpl">
|
||||||
<tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn">
|
<tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn">
|
||||||
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
|
||||||
<td><app-time kind="since" [time]="block.time / 1000" [fastRender]="true"></app-time></td>
|
<td><app-time-since [time]="block.time / 1000" [fastRender]="true"></app-time-since></td>
|
||||||
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
|
||||||
<td class="d-none d-md-block">{{ block.txs.length }}</td>
|
<td class="d-none d-md-block">{{ block.txs.length }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
<td>
|
<td>
|
||||||
‎{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
|
‎{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
|
||||||
<div class="lg-inline">
|
<div class="lg-inline">
|
||||||
<i class="symbol">(<app-time kind="since" [time]="bisqTx.time / 1000" [fastRender]="true"></app-time>)</i>
|
<i class="symbol">(<app-time-since [time]="bisqTx.time / 1000" [fastRender]="true"></app-time-since>)</i>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span>
|
{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
<td><app-time kind="since" [time]="tx.time / 1000" [fastRender]="true"></app-time></td>
|
<td><app-time-since [time]="tx.time / 1000" [fastRender]="true"></app-time-since></td>
|
||||||
<td class="d-none d-md-block"><a [routerLink]="['/block/' | relativeUrl, tx.blockHash]" [state]="{ data: { blockHeight: tx.blockHeight } }">{{ tx.blockHeight }}</a></td>
|
<td class="d-none d-md-block"><a [routerLink]="['/block/' | relativeUrl, tx.blockHash]" [state]="{ data: { blockHeight: tx.blockHeight } }">{{ tx.blockHeight }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -254,30 +254,3 @@ export function selectPowerOfTen(val: number): { divider: number, unit: string }
|
|||||||
|
|
||||||
return selectedPowerOfTen;
|
return selectedPowerOfTen;
|
||||||
}
|
}
|
||||||
|
|
||||||
const featureActivation = {
|
|
||||||
mainnet: {
|
|
||||||
rbf: 399701,
|
|
||||||
segwit: 477120,
|
|
||||||
taproot: 709632,
|
|
||||||
},
|
|
||||||
testnet: {
|
|
||||||
rbf: 720255,
|
|
||||||
segwit: 872730,
|
|
||||||
taproot: 2032291,
|
|
||||||
},
|
|
||||||
signet: {
|
|
||||||
rbf: 0,
|
|
||||||
segwit: 0,
|
|
||||||
taproot: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function isFeatureActive(network: string, height: number, feature: 'rbf' | 'segwit' | 'taproot'): boolean {
|
|
||||||
const activationHeight = featureActivation[network || 'mainnet']?.[feature];
|
|
||||||
if (activationHeight != null) {
|
|
||||||
return height >= activationHeight;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,23 +13,7 @@
|
|||||||
<p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
|
<p i18n>Our mempool and blockchain explorer for the Bitcoin community, focusing on the transaction fee market and multi-layer ecosystem, completely self-hosted without any trusted third-parties.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<video src="/resources/promo-video/mempool-promo.mp4" poster="/resources/promo-video/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true">
|
<video src="/resources/mempool-promo.mp4" poster="/resources/mempool-promo.jpg" controls loop playsinline [autoplay]="true" [muted]="true"></video>
|
||||||
<track label="English" kind="captions" srclang="en" src="/resources/promo-video/en.vtt" [attr.default]="showSubtitles('en') ? '' : null">
|
|
||||||
<track label="日本語" kind="captions" srclang="ja" src="/resources/promo-video/ja.vtt" [attr.default]="showSubtitles('ja') ? '' : null">
|
|
||||||
<track label="中文" kind="captions" srclang="zh" src="/resources/promo-video/zh.vtt" [attr.default]="showSubtitles('zh') ? '' : null">
|
|
||||||
<track label="Svenska" kind="captions" srclang="sv" src="/resources/promo-video/sv.vtt" [attr.default]="showSubtitles('sv') ? '' : null">
|
|
||||||
<track label="Čeština" kind="captions" srclang="cs" src="/resources/promo-video/cs.vtt" [attr.default]="showSubtitles('cs') ? '' : null">
|
|
||||||
<track label="Suomi" kind="captions" srclang="fi" src="/resources/promo-video/fi.vtt" [attr.default]="showSubtitles('fi') ? '' : null">
|
|
||||||
<track label="Français" kind="captions" srclang="fr" src="/resources/promo-video/fr.vtt" [attr.default]="showSubtitles('fr') ? '' : null">
|
|
||||||
<track label="Deutsch" kind="captions" srclang="de" src="/resources/promo-video/de.vtt" [attr.default]="showSubtitles('de') ? '' : null">
|
|
||||||
<track label="Italiano" kind="captions" srclang="it" src="/resources/promo-video/it.vtt" [attr.default]="showSubtitles('it') ? '' : null">
|
|
||||||
<track label="Lietuvių" kind="captions" srclang="lt" src="/resources/promo-video/lt.vtt" [attr.default]="showSubtitles('lt') ? '' : null">
|
|
||||||
<track label="Norsk" kind="captions" srclang="nb" src="/resources/promo-video/nb.vtt" [attr.default]="showSubtitles('nb') ? '' : null">
|
|
||||||
<track label="فارسی" kind="captions" srclang="fa" src="/resources/promo-video/fa.vtt" [attr.default]="showSubtitles('fa') ? '' : null">
|
|
||||||
<track label="Polski" kind="captions" srclang="pl" src="/resources/promo-video/pl.vtt" [attr.default]="showSubtitles('pl') ? '' : null">
|
|
||||||
<track label="Română" kind="captions" srclang="ro" src="/resources/promo-video/ro.vtt" [attr.default]="showSubtitles('ro') ? '' : null">
|
|
||||||
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
|
|
||||||
</video>
|
|
||||||
|
|
||||||
<div class="enterprise-sponsor" id="enterprise-sponsors">
|
<div class="enterprise-sponsor" id="enterprise-sponsors">
|
||||||
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
|
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
|
||||||
@@ -225,7 +209,7 @@
|
|||||||
<span>EmbassyOS</span>
|
<span>EmbassyOS</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank" title="BTCPay Server">
|
<a href="https://github.com/btcpayserver/btcpayserver" target="_blank" title="BTCPay Server">
|
||||||
<img class="image not-rounded" src="/resources/profile/btcpayserver.svg" />
|
<img class="image" src="/resources/profile/btcpayserver.svg" />
|
||||||
<span>BTCPay</span>
|
<span>BTCPay</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/bisq-network/bisq" target="_blank" title="Bisq">
|
<a href="https://github.com/bisq-network/bisq" target="_blank" title="Bisq">
|
||||||
@@ -284,26 +268,6 @@
|
|||||||
<img class="image" src="/resources/profile/nunchuk.svg" />
|
<img class="image" src="/resources/profile/nunchuk.svg" />
|
||||||
<span>Nunchuk</span>
|
<span>Nunchuk</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/bitcoin-s/bitcoin-s" target="_blank" title="bitcoin-s">
|
|
||||||
<img class="image" src="/resources/profile/bitcoin-s.svg" />
|
|
||||||
<span>bitcoin-s</span>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/EdgeApp" target="_blank" title="Edge">
|
|
||||||
<img class="image not-rounded" src="/resources/profile/edge.svg" />
|
|
||||||
<span>Edge</span>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/GaloyMoney" target="_blank" title="Galoy">
|
|
||||||
<img class="image" src="/resources/profile/galoy.svg" />
|
|
||||||
<span>Galoy</span>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/BoltzExchange" target="_blank" title="Boltz">
|
|
||||||
<img class="image" src="/resources/profile/boltz.svg" />
|
|
||||||
<span>Boltz</span>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/MutinyWallet" target="_blank" title="Mutiny">
|
|
||||||
<img class="image not-rounded" src="/resources/profile/mutiny.svg" />
|
|
||||||
<span>Mutiny</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -419,12 +383,6 @@
|
|||||||
<a target="_blank" href="https://matrix.to/#/#mempool:bitcoin.kyoto">
|
<a target="_blank" href="https://matrix.to/#/#mempool:bitcoin.kyoto">
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="matrix" class="svg-inline--fa fa-matrix fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1536 1792"><path fill="currentColor" d="M40.467 163.152v1465.696H145.92V1664H0V128h145.92v35.152zm450.757 464.64v74.14h2.069c19.79-28.356 43.717-50.215 71.483-65.575 27.765-15.656 59.963-23.336 96-23.336 34.56 0 66.165 6.795 94.818 20.086 28.652 13.293 50.216 37.22 65.28 70.893 16.246-23.926 38.4-45.194 66.166-63.507 27.766-18.314 60.848-27.472 98.954-27.472 28.948 0 55.828 3.545 80.64 10.635 24.812 7.088 45.785 18.314 63.508 33.968 17.722 15.656 31.31 35.742 41.354 60.85 9.747 25.107 14.768 55.236 14.768 90.683v366.573h-150.35V865.28c0-18.314-.59-35.741-2.068-51.987-1.476-16.247-5.316-30.426-11.52-42.24-6.499-12.112-15.656-21.563-28.062-28.653-12.405-7.088-29.242-10.634-50.214-10.634-21.268 0-38.4 4.135-51.397 12.112-12.997 8.27-23.336 18.608-30.72 31.901-7.386 12.997-12.407 27.765-14.77 44.602-2.363 16.542-3.84 33.379-3.84 50.216v305.133H692.971v-307.2c0-16.247-.294-32.197-1.18-48.149-.591-15.95-3.84-30.424-9.157-44.011-5.317-13.293-14.178-24.223-26.585-32.197-12.406-7.976-30.425-12.112-54.646-12.112-7.088 0-16.542 1.478-28.062 4.726-11.52 3.25-23.04 9.157-33.968 18.02-10.93 8.86-20.383 21.563-28.063 38.103-7.68 16.543-11.52 38.4-11.52 65.28v317.834H349.44V627.792zm1004.309 1001.056V163.152H1390.08V128H1536v1536h-145.92v-35.152z"/></svg>
|
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="matrix" class="svg-inline--fa fa-matrix fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1536 1792"><path fill="currentColor" d="M40.467 163.152v1465.696H145.92V1664H0V128h145.92v35.152zm450.757 464.64v74.14h2.069c19.79-28.356 43.717-50.215 71.483-65.575 27.765-15.656 59.963-23.336 96-23.336 34.56 0 66.165 6.795 94.818 20.086 28.652 13.293 50.216 37.22 65.28 70.893 16.246-23.926 38.4-45.194 66.166-63.507 27.766-18.314 60.848-27.472 98.954-27.472 28.948 0 55.828 3.545 80.64 10.635 24.812 7.088 45.785 18.314 63.508 33.968 17.722 15.656 31.31 35.742 41.354 60.85 9.747 25.107 14.768 55.236 14.768 90.683v366.573h-150.35V865.28c0-18.314-.59-35.741-2.068-51.987-1.476-16.247-5.316-30.426-11.52-42.24-6.499-12.112-15.656-21.563-28.062-28.653-12.405-7.088-29.242-10.634-50.214-10.634-21.268 0-38.4 4.135-51.397 12.112-12.997 8.27-23.336 18.608-30.72 31.901-7.386 12.997-12.407 27.765-14.77 44.602-2.363 16.542-3.84 33.379-3.84 50.216v305.133H692.971v-307.2c0-16.247-.294-32.197-1.18-48.149-.591-15.95-3.84-30.424-9.157-44.011-5.317-13.293-14.178-24.223-26.585-32.197-12.406-7.976-30.425-12.112-54.646-12.112-7.088 0-16.542 1.478-28.062 4.726-11.52 3.25-23.04 9.157-33.968 18.02-10.93 8.86-20.383 21.563-28.063 38.103-7.68 16.543-11.52 38.4-11.52 65.28v317.834H349.44V627.792zm1004.309 1001.056V163.152H1390.08V128H1536v1536h-145.92v-35.152z"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<a target="_blank" href="https://youtube.com/@mempool">
|
|
||||||
<svg aria-hidden="true" focusable="false" data-prefix="fab" data-icon="youtube" class="svg-inline--fa fa-youtube fa-w-16 fa-2x" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"/></svg>
|
|
||||||
</a>
|
|
||||||
<a target="_blank" href="https://bitcointv.com/c/mempool/videos" class="bitcointv">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" focusable="false" viewBox="0 0 440 440"><path d="M225.57,2.08l-.69-.45a4.22,4.22,0,0,0-5.72,1.23L182.33,46.09a4,4,0,0,0,.88,5.81l9.38,6.38L173.48,97.49a4.22,4.22,0,0,0,2.45,4.19s3.55.7,4.53-1l41.92-40.56a3.62,3.62,0,0,0-1.51-5.1l-10.55-6.12L227.44,6.79A4.26,4.26,0,0,0,225.57,2.08Z" fill="currentColor"></path><path d="M118.52,401.83c-62.51,0-113.37-51-113.37-113.67V214.68C5.15,152,56,101,118.52,101H342.08a24.82,24.82,0,0,1,24.76,24.83V377a24.81,24.81,0,0,1-24.76,24.82Z"></path><path d="M342.08,105.18a20.65,20.65,0,0,1,20.61,20.66V377a20.66,20.66,0,0,1-20.61,20.66H118.52C58.3,397.67,9.31,348.55,9.31,288.16V214.68c0-60.38,49-109.5,109.21-109.5H342.08m0-8.34H118.52C53.62,96.84,1,149.6,1,214.68v73.48C1,353.24,53.62,406,118.52,406H342.08A29,29,0,0,0,371,377V125.84a29,29,0,0,0-28.92-29Z" fill="currentColor"></path><path fill="currentColor" d="M344.69,346.23A25.84,25.84,0,1,0,335,369.87l-10.22-10.2a11.69,11.69,0,1,1,4.77-5.12l10.31,10.28A25.84,25.84,0,0,0,344.69,346.23Z"></path><path fill="currentColor" d="M315.82,257.61a25.67,25.67,0,0,0-12.53,5.22L315,274.49a9.58,9.58,0,0,1,2.11-.73A9.72,9.72,0,1,1,309.4,283a9.4,9.4,0,0,1,.75-3.41L298.4,267.84a25.77,25.77,0,1,0,17.42-10.23Z"></path><path fill="currentColor" d="M313,214a7.76,7.76,0,1,1,1.41,10.91,7.62,7.62,0,0,1-2.19-2.69l-18.67-.14a25.94,25.94,0,1,0,.05-7l18.64.14A7.4,7.4,0,0,1,313,214Z"></path><path fill="currentColor" d="M341.2,144.08h-6.32c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.32c1.67,0,3.61-1.87,3.61-4.29S342.87,144.08,341.2,144.08Z"></path><path fill="currentColor" d="M301.75,144.08h-6.44c-1.67,0-3.61,1.87-3.61,4.29s1.94,4.29,3.61,4.29h6.44c1.67,0,3.61-1.87,3.61-4.29S303.42,144.08,301.75,144.08Z"></path><path fill="currentColor" d="M321.77,144.08h-7c-1.67,0-3.62,1.87-3.62,4.29s1.95,4.29,3.62,4.29h7c1.67,0,3.62-1.87,3.62-4.29S323.44,144.08,321.77,144.08Z"></path><ellipse fill="currentColor" cx="295.97" cy="127.61" rx="4.27" ry="4.29"></ellipse><path fill="currentColor" d="M340.54,131.9a4.29,4.29,0,1,0-4.27-4.29A4.28,4.28,0,0,0,340.54,131.9Z"></path><path fill="currentColor" d="M318.26,131.9a4.29,4.29,0,1,0-4.27-4.29A4.29,4.29,0,0,0,318.26,131.9Z"></path><ellipse fill="currentColor" cx="295.97" cy="169.13" rx="4.27" ry="4.29"></ellipse><path fill="currentColor" d="M340.54,164.84a4.3,4.3,0,1,0,4.27,4.29A4.29,4.29,0,0,0,340.54,164.84Z"></path><path fill="currentColor" d="M318.26,164.84a4.3,4.3,0,1,0,4.28,4.29A4.29,4.29,0,0,0,318.26,164.84Z"></path><path d="M108.62,256.87c8.36-1,7.68-7.76,3.14-17-3.64-7.4-9.74-16.39-15.75-25.36-14.23-21.23-27.69-42.23-5.35-41.07,19.55,1,42.9,18.63,68.22,36.74,31.1,22.24,65.16,45.21,98.81,39.11a151.19,151.19,0,0,1,20-2.37V221a92,92,0,0,0-91.91-92.16H124.33A92,92,0,0,0,32.42,221v17.59c17.71,3.81,31,9.94,43.8,14.15C86.6,256.16,96.69,258.31,108.62,256.87Z"></path><path d="M273.37,310.79c-35-15.26-76.67-32.1-104-23.59-3.15,1-5,2.3-6,3.85-3.35,5.31,4.67,13.57,14.89,22.17,7.17,6,15.36,12.21,21.44,17.64,11.47,10.26,15.35,17.84-9.89,16.62-29.75-1.44-49.18-13.75-71.18-24l-.29-.14a165.84,165.84,0,0,0-22.93-8.91c-15.74-4.67-34.22-6.79-58.51-3.28A91.93,91.93,0,0,0,124.33,375h61.45A92,92,0,0,0,273.37,310.79Z"></path><path fill="currentColor" d="M257.69,249.31C224,255.41,190,232.44,158.88,210.2c-25.32-18.11-48.67-35.72-68.22-36.74C68.32,172.3,81.78,193.3,96,214.53c6,9,12.11,18,15.75,25.36,4.54,9.22,5.22,16-3.14,17-11.93,1.44-22-.71-32.4-4.13-12.8-4.21-26.09-10.34-43.8-14.15v44.26c0,1.26.14,2.48.19,3.72a91.8,91.8,0,0,0,2.9,19.62c.43,1.67.84,3.34,1.37,5,24.29-3.51,42.77-1.39,58.51,3.28a165.84,165.84,0,0,1,22.93,8.91c.39-.12.76-.26,1.14-.39l-.85.53c22,10.25,41.43,22.56,71.18,24,25.24,1.22,21.36-6.36,9.89-16.62-6.08-5.43-14.27-11.61-21.44-17.64-10.22-8.6-18.24-16.86-14.89-22.17,1-1.55,2.87-2.87,6-3.85,27.33-8.51,69,8.33,104,23.59.32-1,.56-2.05.84-3.07a92.33,92.33,0,0,0,3.48-24.87V246.94A151.19,151.19,0,0,0,257.69,249.31Z"></path><path fill="currentColor" d="M192,137a78,78,0,0,1,77.78,78v73.91a78,78,0,0,1-77.78,78H118.51a78,78,0,0,1-77.78-78V215a78,78,0,0,1,77.78-78H192m0-8.33H118.51A86.21,86.21,0,0,0,32.42,215v73.91a86.21,86.21,0,0,0,86.09,86.33H192a86.21,86.21,0,0,0,86.09-86.33V215A86.21,86.21,0,0,0,192,128.64Z"></path></svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,6 @@
|
|||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image.not-rounded {
|
|
||||||
border-radius: 0;
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro {
|
.intro {
|
||||||
margin: 25px auto 30px;
|
margin: 25px auto 30px;
|
||||||
width: 250px;
|
width: 250px;
|
||||||
@@ -42,11 +36,9 @@
|
|||||||
|
|
||||||
video {
|
video {
|
||||||
width: 640px;
|
width: 640px;
|
||||||
|
height: 360px;
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
@media (min-width: 768px) {
|
|
||||||
height: 360px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.social-icons {
|
.social-icons {
|
||||||
@@ -59,13 +51,9 @@
|
|||||||
.enterprise-sponsor,
|
.enterprise-sponsor,
|
||||||
.community-integrations-sponsor,
|
.community-integrations-sponsor,
|
||||||
.maintainers {
|
.maintainers {
|
||||||
margin-top: 30px;
|
margin-top: 68px;
|
||||||
margin-bottom: 68px;
|
margin-bottom: 68px;
|
||||||
scroll-margin: 30px;
|
scroll-margin: 30px;
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
margin-top: 68px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.maintainers {
|
.maintainers {
|
||||||
@@ -211,16 +199,6 @@
|
|||||||
a {
|
a {
|
||||||
margin: 45px 10px;
|
margin: 45px 10px;
|
||||||
}
|
}
|
||||||
.bitcointv svg {
|
|
||||||
width: 36px;
|
|
||||||
height: auto;
|
|
||||||
vertical-align: bottom;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
.bitcointv svg:hover {
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,11 +212,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.community-integrations-sponsor {
|
.community-integrations-sponsor {
|
||||||
max-width: 1110px;
|
max-width: 965px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.community-integrations-sponsor img.image {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export class AboutComponent implements OnInit {
|
|||||||
tap(() => this.goToAnchor())
|
tap(() => this.goToAnchor())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.goToAnchor();
|
this.goToAnchor();
|
||||||
}
|
}
|
||||||
@@ -90,8 +90,4 @@ export class AboutComponent implements OnInit {
|
|||||||
this.showNavigateToSponsor = true;
|
this.showNavigateToSponsor = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showSubtitles(language) {
|
|
||||||
return ( this.locale.startsWith( language ) && !this.locale.startsWith('en') );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
|
<ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
|
||||||
<span class="fiat" *ngIf="blockConversion; else noblockconversion">
|
<span class="fiat" *ngIf="blockConversion; else noblockconversion">
|
||||||
{{ addPlus && satoshis >= 0 ? '+' : '' }}{{
|
{{ addPlus && satoshis >= 0 ? '+' : '' }}
|
||||||
|
{{
|
||||||
(
|
(
|
||||||
(blockConversion.price[currency] > -1 ? blockConversion.price[currency] : null) ??
|
(blockConversion.price[currency] >= 0 ? blockConversion.price[currency] : null) ??
|
||||||
(blockConversion.price['USD'] > -1 ? blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency] : null) ?? 0
|
(blockConversion.price['USD'] * blockConversion.exchangeRates['USD' + currency]) ?? 0
|
||||||
) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency
|
) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<ng-template #noblockconversion>
|
<ng-template #noblockconversion>
|
||||||
<span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions[currency] > -1 ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}
|
<span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}</span>
|
||||||
</span>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,31 @@
|
|||||||
max-height: 270px;
|
max-height: 270px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.formRadioGroup {
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
@media (min-width: 991px) {
|
||||||
|
position: relative;
|
||||||
|
top: -100px;
|
||||||
|
}
|
||||||
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
|
position: relative;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
@media (min-width: 830px) {
|
||||||
|
flex-direction: row;
|
||||||
|
float: right;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 9px;
|
||||||
|
@media (min-width: 830px) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.disabled {
|
.disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|||||||
@@ -54,6 +54,31 @@
|
|||||||
max-height: 270px;
|
max-height: 270px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.formRadioGroup {
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
@media (min-width: 991px) {
|
||||||
|
position: relative;
|
||||||
|
top: -100px;
|
||||||
|
}
|
||||||
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
|
position: relative;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
@media (min-width: 830px) {
|
||||||
|
flex-direction: row;
|
||||||
|
float: right;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 9px;
|
||||||
|
@media (min-width: 830px) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.disabled {
|
.disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|||||||
@@ -54,6 +54,31 @@
|
|||||||
max-height: 270px;
|
max-height: 270px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.formRadioGroup {
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
@media (min-width: 991px) {
|
||||||
|
position: relative;
|
||||||
|
top: -100px;
|
||||||
|
}
|
||||||
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
|
position: relative;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
@media (min-width: 830px) {
|
||||||
|
flex-direction: row;
|
||||||
|
float: right;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 9px;
|
||||||
|
@media (min-width: 830px) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.disabled {
|
.disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|||||||
@@ -54,6 +54,31 @@
|
|||||||
max-height: 270px;
|
max-height: 270px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.formRadioGroup {
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
@media (min-width: 1130px) {
|
||||||
|
position: relative;
|
||||||
|
top: -100px;
|
||||||
|
}
|
||||||
|
@media (min-width: 830px) and (max-width: 1130px) {
|
||||||
|
position: relative;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
@media (min-width: 830px) {
|
||||||
|
flex-direction: row;
|
||||||
|
float: right;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 9px;
|
||||||
|
@media (min-width: 830px) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.disabled {
|
.disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
<ng-container *ngIf="!isLoadingBlock; else loadingRest">
|
<ng-container *ngIf="!isLoadingBlock; else loadingRest">
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
<td i18n="mempool-block.fee-span">Fee span</td>
|
<td i18n="mempool-block.fee-span">Fee span</td>
|
||||||
<td><span>{{ block.extras.feeRange[1] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
|
<td><span>{{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="block?.extras?.medianFee != undefined">
|
<tr *ngIf="block?.extras?.medianFee != undefined">
|
||||||
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div [attr.data-cy]="'bitcoin-block-offset-' + offset + '-index-' + i"
|
<div [attr.data-cy]="'bitcoin-block-offset-' + offset + '-index-' + i"
|
||||||
class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ i }}"
|
class="text-center bitcoin-block mined-block blockchain-blocks-offset-{{ offset }}-index-{{ i }}"
|
||||||
id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
|
id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
|
||||||
[class.blink-bg]="isSpecial(block.height)">
|
[class.blink-bg]="(specialBlocks[block.height] !== undefined)">
|
||||||
<a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
|
<a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
|
||||||
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
||||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
|
<div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
||||||
</div>
|
</div>
|
||||||
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference">
|
<div [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-time'" class="time-difference">
|
||||||
<app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time></div>
|
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
|
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
|
||||||
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
|
<a [attr.data-cy]="'bitcoin-block-' + offset + '-index-' + i + '-pool'" class="badge badge-primary"
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ interface BlockchainBlock extends BlockExtended {
|
|||||||
export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
@Input() static: boolean = false;
|
@Input() static: boolean = false;
|
||||||
@Input() offset: number = 0;
|
@Input() offset: number = 0;
|
||||||
@Input() height: number = 0; // max height of blocks in chunk (dynamic blocks only)
|
@Input() height: number = 0;
|
||||||
@Input() count: number = 8; // number of blocks in this chunk (dynamic blocks only)
|
@Input() count: number = 8;
|
||||||
@Input() loadingTip: boolean = false;
|
@Input() loadingTip: boolean = false;
|
||||||
@Input() connected: boolean = true;
|
@Input() connected: boolean = true;
|
||||||
|
|
||||||
@@ -31,7 +31,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
dynamicBlocksAmount: number = 8;
|
dynamicBlocksAmount: number = 8;
|
||||||
emptyBlocks: BlockExtended[] = this.mountEmptyBlocks();
|
emptyBlocks: BlockExtended[] = this.mountEmptyBlocks();
|
||||||
markHeight: number;
|
markHeight: number;
|
||||||
chainTip: number;
|
|
||||||
blocksSubscription: Subscription;
|
blocksSubscription: Subscription;
|
||||||
blockPageSubscription: Subscription;
|
blockPageSubscription: Subscription;
|
||||||
networkSubscription: Subscription;
|
networkSubscription: Subscription;
|
||||||
@@ -74,7 +73,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.chainTip = this.stateService.latestBlockHeight;
|
|
||||||
this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
|
this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
|
||||||
|
|
||||||
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
||||||
@@ -109,7 +107,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.blocks.unshift(block);
|
this.blocks.unshift(block);
|
||||||
this.blocks = this.blocks.slice(0, this.dynamicBlocksAmount);
|
this.blocks = this.blocks.slice(0, this.dynamicBlocksAmount);
|
||||||
|
|
||||||
if (txConfirmed && block.height > this.chainTip) {
|
if (txConfirmed) {
|
||||||
this.markHeight = block.height;
|
this.markHeight = block.height;
|
||||||
this.moveArrowToPosition(true, true);
|
this.moveArrowToPosition(true, true);
|
||||||
} else {
|
} else {
|
||||||
@@ -117,7 +115,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.blockStyles = [];
|
this.blockStyles = [];
|
||||||
if (this.blocksFilled && block.height > this.chainTip) {
|
if (this.blocksFilled) {
|
||||||
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205)));
|
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205)));
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.blockStyles = [];
|
this.blockStyles = [];
|
||||||
@@ -131,8 +129,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
if (this.blocks.length === this.dynamicBlocksAmount) {
|
if (this.blocks.length === this.dynamicBlocksAmount) {
|
||||||
this.blocksFilled = true;
|
this.blocksFilled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.chainTip = Math.max(this.chainTip, block.height);
|
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -269,10 +265,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
isSpecial(height: number): boolean {
|
|
||||||
return this.specialBlocks[height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
getStyleForBlock(block: BlockchainBlock, index: number, animateEnterFrom: number = 0) {
|
getStyleForBlock(block: BlockchainBlock, index: number, animateEnterFrom: number = 0) {
|
||||||
if (!block || block.placeholder) {
|
if (!block || block.placeholder) {
|
||||||
return this.getStyleForPlaceholderBlock(index, animateEnterFrom);
|
return this.getStyleForPlaceholderBlock(index, animateEnterFrom);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
<tbody *ngIf="blocks$ | async as blocks; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||||
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
|
<tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
|
||||||
<td class="height text-left" [class]="widget ? 'widget' : ''">
|
<td class="text-left" [class]="widget ? 'widget' : ''">
|
||||||
<a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a>
|
<a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||||
</td>
|
</td>
|
||||||
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||||
<app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time>
|
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||||
<a
|
<a
|
||||||
@@ -89,6 +89,7 @@
|
|||||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
<td *ngIf="indexingAvailable" class="pool text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||||
|
<img width="1" height="25" style="opacity: 0">
|
||||||
<span class="skeleton-loader" style="max-width: 125px"></span>
|
<span class="skeleton-loader" style="max-width: 125px"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
<td class="timestamp" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||||
@@ -97,7 +98,7 @@
|
|||||||
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
<td class="mined" *ngIf="!widget" [class]="indexingAvailable ? '' : 'legacy'">
|
||||||
<span class="skeleton-loader" style="max-width: 125px"></span>
|
<span class="skeleton-loader" style="max-width: 125px"></span>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="auditAvailable" class="health text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
<td *ngIf="auditAvailable" class="health text-left" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
<td *ngIf="indexingAvailable" class="reward text-right" [ngClass]="{'widget': widget, 'legacy': !indexingAvailable}">
|
||||||
|
|||||||
@@ -51,12 +51,7 @@ tr, td, th {
|
|||||||
.pool.widget {
|
.pool.widget {
|
||||||
width: 40%;
|
width: 40%;
|
||||||
padding-left: 24px;
|
padding-left: 24px;
|
||||||
@media (min-width: 768px) AND (max-width: 926px) {
|
@media (max-width: 376px) {
|
||||||
padding-left: 0px;
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
@media (max-width: 430px) {
|
|
||||||
padding-left: 0px;
|
|
||||||
width: 60%;
|
width: 60%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,10 +59,6 @@ tr, td, th {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: text-top;
|
vertical-align: text-top;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 160px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.height {
|
.height {
|
||||||
@@ -78,12 +69,6 @@ tr, td, th {
|
|||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
width: 10%;
|
width: 10%;
|
||||||
}
|
}
|
||||||
@media (min-width: 768px) AND (max-width: 926px) {
|
|
||||||
width: 30%;
|
|
||||||
}
|
|
||||||
@media (max-width: 430px) {
|
|
||||||
width: 30%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.height.legacy {
|
.height.legacy {
|
||||||
width: 15%;
|
width: 15%;
|
||||||
@@ -107,7 +92,7 @@ tr, td, th {
|
|||||||
|
|
||||||
.mined {
|
.mined {
|
||||||
width: 13%;
|
width: 13%;
|
||||||
@media (max-width: 730px) {
|
@media (max-width: 576px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,7 +138,7 @@ tr, td, th {
|
|||||||
|
|
||||||
.fees {
|
.fees {
|
||||||
width: 8%;
|
width: 8%;
|
||||||
@media (max-width: 820px) {
|
@media (max-width: 650px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,16 +163,6 @@ tr, td, th {
|
|||||||
width: 30%;
|
width: 30%;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
@media (min-width: 768px) AND (max-width: 926px) {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 90px;
|
|
||||||
}
|
|
||||||
@media (max-width: 430px) {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: 90px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.size {
|
.size {
|
||||||
@@ -214,10 +189,10 @@ tr, td, th {
|
|||||||
|
|
||||||
.health {
|
.health {
|
||||||
width: 10%;
|
width: 10%;
|
||||||
@media (max-width: 1105px) {
|
@media (max-width: 1000px) {
|
||||||
width: 13%;
|
width: 13%;
|
||||||
}
|
}
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 950px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +202,7 @@ tr, td, th {
|
|||||||
}
|
}
|
||||||
.health.widget {
|
.health.widget {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
@media (max-width: 1105px) {
|
@media (max-width: 1000px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
@@ -267,4 +242,4 @@ tr, td, th {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
max-width: 50vw;
|
max-width: 50vw;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
@@ -87,8 +87,8 @@ export class BlocksList implements OnInit, OnDestroy {
|
|||||||
this.stateService.blocks$
|
this.stateService.blocks$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((block) => {
|
switchMap((block) => {
|
||||||
if (block[0].height <= this.lastBlockHeight) {
|
if (block[0].height < this.lastBlockHeight) {
|
||||||
return [null]; // Return an empty stream so the last pipe is not executed
|
return []; // Return an empty stream so the last pipe is not executed
|
||||||
}
|
}
|
||||||
this.lastBlockHeight = block[0].height;
|
this.lastBlockHeight = block[0].height;
|
||||||
return [block];
|
return [block];
|
||||||
@@ -101,16 +101,14 @@ export class BlocksList implements OnInit, OnDestroy {
|
|||||||
this.lastPage = this.page;
|
this.lastPage = this.page;
|
||||||
return blocks[0];
|
return blocks[0];
|
||||||
}
|
}
|
||||||
if (blocks[1]) {
|
this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height) + 1;
|
||||||
this.blocksCount = Math.max(this.blocksCount, blocks[1][0].height) + 1;
|
if (this.stateService.env.MINING_DASHBOARD) {
|
||||||
if (this.stateService.env.MINING_DASHBOARD) {
|
// @ts-ignore: Need to add an extra field for the template
|
||||||
// @ts-ignore: Need to add an extra field for the template
|
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
|
||||||
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
|
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
||||||
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
|
||||||
}
|
|
||||||
acc.unshift(blocks[1][0]);
|
|
||||||
acc = acc.slice(0, this.widget ? 6 : 15);
|
|
||||||
}
|
}
|
||||||
|
acc.unshift(blocks[1][0]);
|
||||||
|
acc = acc.slice(0, this.widget ? 6 : 15);
|
||||||
return acc;
|
return acc;
|
||||||
}, [])
|
}, [])
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,19 +2,18 @@
|
|||||||
<table class="table latest-adjustments">
|
<table class="table latest-adjustments">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="" i18n="block.height">Height</th>
|
<th class="d-none d-md-block" i18n="block.height">Height</th>
|
||||||
<th class="date text-left" i18n="mining.adjusted">Adjusted</th>
|
<th i18n="mining.adjusted" class="text-left">Adjusted</th>
|
||||||
<th class="text-right" i18n="mining.difficulty">Difficulty</th>
|
<th i18n="mining.difficulty" class="text-right">Difficulty</th>
|
||||||
<th class="text-right" i18n="mining.change">Change</th>
|
<th i18n="mining.change" class="text-right">Change</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody *ngIf="(hashrateObservable$ | async) as data">
|
<tbody *ngIf="(hashrateObservable$ | async) as data">
|
||||||
<tr *ngFor="let diffChange of data">
|
<tr *ngFor="let diffChange of data">
|
||||||
<td class="">
|
<td class="d-none d-md-block"><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height
|
||||||
<a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a>
|
}}</a></td>
|
||||||
</td>
|
<td class="text-left">
|
||||||
<td class="date text-left">
|
<app-time-since [time]="diffChange.timestamp" [fastRender]="true"></app-time-since>
|
||||||
<app-time kind="since" [time]="diffChange.timestamp" [fastRender]="true"></app-time>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">{{ diffChange.difficultyShorten }}</td>
|
<td class="text-right">{{ diffChange.difficultyShorten }}</td>
|
||||||
<td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">
|
<td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">
|
||||||
@@ -24,8 +23,8 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
<tbody *ngIf="isLoading">
|
<tbody *ngIf="isLoading">
|
||||||
<tr *ngFor="let item of [1,2,3,4,5,6]">
|
<tr *ngFor="let item of [1,2,3,4,5,6]">
|
||||||
<td class=""><span class="skeleton-loader"></span></td>
|
<td class="d-none d-md-block w-75"><span class="skeleton-loader"></span></td>
|
||||||
<td class="date text-left"><span class="skeleton-loader w-75"></span></td>
|
<td class="text-left"><span class="skeleton-loader w-75"></span></td>
|
||||||
<td class="text-right"><span class="skeleton-loader w-75"></span></td>
|
<td class="text-right"><span class="skeleton-loader w-75"></span></td>
|
||||||
<td class="text-right"><span class="skeleton-loader w-75"></span></td>
|
<td class="text-right"><span class="skeleton-loader w-75"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -17,12 +17,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
|
||||||
@media (min-width: 767px) AND (max-width: 991px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@media (max-width: 500px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<div *ngIf="showTitle" class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
|
|
||||||
<div class="card-wrapper">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body more-padding">
|
|
||||||
<div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty">
|
|
||||||
<div class="item">
|
|
||||||
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
|
|
||||||
<div class="card-text">
|
|
||||||
<ng-container *ngTemplateOutlet="epochData.remainingBlocks === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.remainingBlocks }"></ng-container>
|
|
||||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
|
||||||
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
|
||||||
</div>
|
|
||||||
<div class="symbol"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
|
|
||||||
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
|
|
||||||
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
|
|
||||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
|
||||||
</span>
|
|
||||||
<ng-template #arrowDownDifficulty >
|
|
||||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
|
||||||
</ng-template>
|
|
||||||
{{ epochData.change | absolute | number: '1.2-2' }}
|
|
||||||
<span class="symbol">%</span>
|
|
||||||
</div>
|
|
||||||
<ng-template #recentlyAdjusted>
|
|
||||||
<div class="card-text">—</div>
|
|
||||||
</ng-template>
|
|
||||||
<div class="symbol">
|
|
||||||
<span i18n="difficulty-box.previous">Previous</span>:
|
|
||||||
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
|
|
||||||
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
|
|
||||||
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
|
||||||
</span>
|
|
||||||
<ng-template #arrowDownPreviousDifficulty >
|
|
||||||
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
|
||||||
</ng-template>
|
|
||||||
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> %
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="item" *ngIf="showProgress">
|
|
||||||
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
|
|
||||||
<div class="card-text">{{ epochData.progress | number: '1.2-2' }} <span class="symbol">%</span></div>
|
|
||||||
<div class="progress small-bar">
|
|
||||||
<div class="progress-bar" role="progressbar" style="width: 15%; background-color: #105fb0" [ngStyle]="{'width': epochData.base}"> </div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="item" *ngIf="showHalving">
|
|
||||||
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
|
|
||||||
<div class="card-text">
|
|
||||||
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
|
|
||||||
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
|
||||||
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
|
||||||
</div>
|
|
||||||
<div class="symbol"><app-time kind="until" [time]="epochData.timeUntilHalving" [fastRender]="true"></app-time></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-template #loadingDifficulty>
|
|
||||||
<div class="difficulty-skeleton loading-container">
|
|
||||||
<div class="item">
|
|
||||||
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
|
|
||||||
<div class="card-text">
|
|
||||||
<div class="skeleton-loader"></div>
|
|
||||||
<div class="skeleton-loader"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
|
|
||||||
<div class="card-text">
|
|
||||||
<div class="skeleton-loader"></div>
|
|
||||||
<div class="skeleton-loader"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="item">
|
|
||||||
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
|
|
||||||
<div class="card-text">
|
|
||||||
<div class="skeleton-loader"></div>
|
|
||||||
<div class="skeleton-loader"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
.difficulty-adjustment-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-around;
|
|
||||||
height: 76px;
|
|
||||||
.shared-block {
|
|
||||||
color: #ffffff66;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.item {
|
|
||||||
padding: 0 5px;
|
|
||||||
width: 100%;
|
|
||||||
&:nth-child(1) {
|
|
||||||
display: none;
|
|
||||||
@media (min-width: 485px) {
|
|
||||||
display: table-cell;
|
|
||||||
}
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
display: table-cell;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.card-text {
|
|
||||||
font-size: 22px;
|
|
||||||
margin-top: -9px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.difficulty-skeleton {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
@media (min-width: 376px) {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
.item {
|
|
||||||
min-width: 120px;
|
|
||||||
max-width: 150px;
|
|
||||||
margin: 0;
|
|
||||||
width: -webkit-fill-available;
|
|
||||||
@media (min-width: 376px) {
|
|
||||||
margin: 0 auto 0px;
|
|
||||||
}
|
|
||||||
&:first-child{
|
|
||||||
display: none;
|
|
||||||
@media (min-width: 485px) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@media (min-width: 992px) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.card-text {
|
|
||||||
.skeleton-loader {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
&:first-child {
|
|
||||||
margin: 14px auto 0;
|
|
||||||
max-width: 80px;
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
margin: 10px auto 0;
|
|
||||||
max-width: 120px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background-color: #1d1f31;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
color: #4a68b9;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
display: inline-flex;
|
|
||||||
width: 100%;
|
|
||||||
background-color: #2d3348;
|
|
||||||
height: 1.1rem;
|
|
||||||
max-width: 180px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-loader {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.more-padding {
|
|
||||||
padding: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.small-bar {
|
|
||||||
height: 8px;
|
|
||||||
top: -4px;
|
|
||||||
max-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-container {
|
|
||||||
min-height: 76px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-title {
|
|
||||||
position: relative;
|
|
||||||
color: #ffffff91;
|
|
||||||
margin-top: -13px;
|
|
||||||
font-size: 10px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: center;
|
|
||||||
padding-bottom: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-wrapper {
|
|
||||||
.card {
|
|
||||||
height: auto !important;
|
|
||||||
}
|
|
||||||
.card-body {
|
|
||||||
display: flex;
|
|
||||||
flex: inherit;
|
|
||||||
text-align: center;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-around;
|
|
||||||
padding: 24px 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.retarget-sign {
|
|
||||||
margin-right: -3px;
|
|
||||||
font-size: 14px;
|
|
||||||
top: -2px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.previous-retarget-sign {
|
|
||||||
margin-right: -2px;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.symbol {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
|
||||||
import { combineLatest, Observable, timer } from 'rxjs';
|
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
|
||||||
import { StateService } from '../../services/state.service';
|
|
||||||
|
|
||||||
interface EpochProgress {
|
|
||||||
base: string;
|
|
||||||
change: number;
|
|
||||||
progress: number;
|
|
||||||
remainingBlocks: number;
|
|
||||||
newDifficultyHeight: number;
|
|
||||||
colorAdjustments: string;
|
|
||||||
colorPreviousAdjustments: string;
|
|
||||||
estimatedRetargetDate: number;
|
|
||||||
previousRetarget: number;
|
|
||||||
blocksUntilHalving: number;
|
|
||||||
timeUntilHalving: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-difficulty-mining',
|
|
||||||
templateUrl: './difficulty-mining.component.html',
|
|
||||||
styleUrls: ['./difficulty-mining.component.scss'],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
})
|
|
||||||
export class DifficultyMiningComponent implements OnInit {
|
|
||||||
isLoadingWebSocket$: Observable<boolean>;
|
|
||||||
difficultyEpoch$: Observable<EpochProgress>;
|
|
||||||
|
|
||||||
@Input() showProgress = true;
|
|
||||||
@Input() showHalving = false;
|
|
||||||
@Input() showTitle = true;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public stateService: StateService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
|
|
||||||
this.difficultyEpoch$ = combineLatest([
|
|
||||||
this.stateService.blocks$.pipe(map(([block]) => block)),
|
|
||||||
this.stateService.difficultyAdjustment$,
|
|
||||||
])
|
|
||||||
.pipe(
|
|
||||||
map(([block, da]) => {
|
|
||||||
let colorAdjustments = '#ffffff66';
|
|
||||||
if (da.difficultyChange > 0) {
|
|
||||||
colorAdjustments = '#3bcc49';
|
|
||||||
}
|
|
||||||
if (da.difficultyChange < 0) {
|
|
||||||
colorAdjustments = '#dc3545';
|
|
||||||
}
|
|
||||||
|
|
||||||
let colorPreviousAdjustments = '#dc3545';
|
|
||||||
if (da.previousRetarget) {
|
|
||||||
if (da.previousRetarget >= 0) {
|
|
||||||
colorPreviousAdjustments = '#3bcc49';
|
|
||||||
}
|
|
||||||
if (da.previousRetarget === 0) {
|
|
||||||
colorPreviousAdjustments = '#ffffff66';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
colorPreviousAdjustments = '#ffffff66';
|
|
||||||
}
|
|
||||||
|
|
||||||
const blocksUntilHalving = 210000 - (block.height % 210000);
|
|
||||||
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
base: `${da.progressPercent.toFixed(2)}%`,
|
|
||||||
change: da.difficultyChange,
|
|
||||||
progress: da.progressPercent,
|
|
||||||
remainingBlocks: da.remainingBlocks - 1,
|
|
||||||
colorAdjustments,
|
|
||||||
colorPreviousAdjustments,
|
|
||||||
newDifficultyHeight: da.nextRetargetHeight,
|
|
||||||
estimatedRetargetDate: da.estimatedRetargetDate,
|
|
||||||
previousRetarget: da.previousRetarget,
|
|
||||||
blocksUntilHalving,
|
|
||||||
timeUntilHalving,
|
|
||||||
};
|
|
||||||
return data;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<div
|
|
||||||
#tooltip
|
|
||||||
*ngIf="status"
|
|
||||||
class="difficulty-tooltip"
|
|
||||||
[style.visibility]="status ? 'visible' : 'hidden'"
|
|
||||||
[style.left]="tooltipPosition.x + 'px'"
|
|
||||||
[style.top]="tooltipPosition.y + 'px'"
|
|
||||||
>
|
|
||||||
<ng-container [ngSwitch]="status">
|
|
||||||
<ng-container *ngSwitchCase="'mined'">
|
|
||||||
<ng-container *ngIf="isAhead">
|
|
||||||
<ng-container *ngTemplateOutlet="expected === 1 ? blocksSingular : blocksPlural; context: {$implicit: expected }"></ng-container>
|
|
||||||
<ng-template #blocksPlural let-i i18n="difficulty-box.expected-blocks">{{ i }} blocks expected</ng-template>
|
|
||||||
<ng-template #blocksSingular let-i i18n="difficulty-box.expected-block">{{ i }} block expected</ng-template>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="!isAhead">
|
|
||||||
<ng-container *ngTemplateOutlet="mined === 1 ? blocksSingular : blocksPlural; context: {$implicit: mined }"></ng-container>
|
|
||||||
<ng-template #blocksPlural let-i i18n="difficulty-box.mined-blocks">{{ i }} blocks mined</ng-template>
|
|
||||||
<ng-template #blocksSingular let-i i18n="difficulty-box.mined-block">{{ i }} block mined</ng-template>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="'remaining'">
|
|
||||||
<ng-container *ngTemplateOutlet="remaining === 1 ? blocksSingular : blocksPlural; context: {$implicit: remaining }"></ng-container>
|
|
||||||
<ng-template #blocksPlural let-i i18n="difficulty-box.remaining-blocks">{{ i }} blocks remaining</ng-template>
|
|
||||||
<ng-template #blocksSingular let-i i18n="difficulty-box.remaining-block">{{ i }} block remaining</ng-template>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="'ahead'">
|
|
||||||
<ng-container *ngTemplateOutlet="ahead === 1 ? blocksSingular : blocksPlural; context: {$implicit: ahead }"></ng-container>
|
|
||||||
<ng-template #blocksPlural let-i i18n="difficulty-box.blocks-ahead">{{ i }} blocks ahead</ng-template>
|
|
||||||
<ng-template #blocksSingular let-i i18n="difficulty-box.block-ahead">{{ i }} block ahead</ng-template>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="'behind'">
|
|
||||||
<ng-container *ngTemplateOutlet="behind === 1 ? blocksSingular : blocksPlural; context: {$implicit: behind }"></ng-container>
|
|
||||||
<ng-template #blocksPlural let-i i18n="difficulty-box.blocks-behind">{{ i }} blocks behind</ng-template>
|
|
||||||
<ng-template #blocksSingular let-i i18n="difficulty-box.block-behind">{{ i }} block behind</ng-template>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="'next'">
|
|
||||||
<span class="next-block" i18n="@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c">Next Block</span>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
.difficulty-tooltip {
|
|
||||||
position: fixed;
|
|
||||||
background: rgba(#11131f, 0.95);
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
|
|
||||||
color: #b1b1b1;
|
|
||||||
padding: 10px 15px;
|
|
||||||
text-align: left;
|
|
||||||
pointer-events: none;
|
|
||||||
max-width: 300px;
|
|
||||||
min-width: 200px;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.next-block {
|
|
||||||
text-transform: lowercase;
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
|
|
||||||
|
|
||||||
interface EpochProgress {
|
|
||||||
base: string;
|
|
||||||
change: number;
|
|
||||||
progress: number;
|
|
||||||
minedBlocks: number;
|
|
||||||
remainingBlocks: number;
|
|
||||||
expectedBlocks: number;
|
|
||||||
newDifficultyHeight: number;
|
|
||||||
colorAdjustments: string;
|
|
||||||
colorPreviousAdjustments: string;
|
|
||||||
estimatedRetargetDate: number;
|
|
||||||
previousRetarget: number;
|
|
||||||
blocksUntilHalving: number;
|
|
||||||
timeUntilHalving: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-difficulty-tooltip',
|
|
||||||
templateUrl: './difficulty-tooltip.component.html',
|
|
||||||
styleUrls: ['./difficulty-tooltip.component.scss'],
|
|
||||||
})
|
|
||||||
export class DifficultyTooltipComponent implements OnChanges {
|
|
||||||
@Input() status: string | void;
|
|
||||||
@Input() progress: EpochProgress | void = null;
|
|
||||||
@Input() cursorPosition: { x: number, y: number };
|
|
||||||
|
|
||||||
mined: number;
|
|
||||||
ahead: number;
|
|
||||||
behind: number;
|
|
||||||
expected: number;
|
|
||||||
remaining: number;
|
|
||||||
isAhead: boolean;
|
|
||||||
isBehind: boolean;
|
|
||||||
|
|
||||||
tooltipPosition = { x: 0, y: 0 };
|
|
||||||
|
|
||||||
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
|
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
ngOnChanges(changes): void {
|
|
||||||
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
|
|
||||||
let x = changes.cursorPosition.currentValue.x;
|
|
||||||
let y = changes.cursorPosition.currentValue.y - 50;
|
|
||||||
if (this.tooltipElement) {
|
|
||||||
const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect();
|
|
||||||
x -= elementBounds.width / 2;
|
|
||||||
x = Math.min(Math.max(x, 20), (window.innerWidth - 20 - elementBounds.width));
|
|
||||||
}
|
|
||||||
this.tooltipPosition = { x, y };
|
|
||||||
}
|
|
||||||
if ((changes.progress || changes.status) && this.progress && this.status) {
|
|
||||||
this.remaining = this.progress.remainingBlocks;
|
|
||||||
this.expected = this.progress.expectedBlocks;
|
|
||||||
this.mined = this.progress.minedBlocks;
|
|
||||||
this.ahead = Math.max(0, this.mined - this.expected);
|
|
||||||
this.behind = Math.max(0, this.expected - this.mined);
|
|
||||||
this.isAhead = this.ahead > 0;
|
|
||||||
this.isBehind = this.behind > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,100 +3,81 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body more-padding">
|
<div class="card-body more-padding">
|
||||||
<div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty">
|
<div class="difficulty-adjustment-container" *ngIf="(isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData; else loadingDifficulty">
|
||||||
<div class="epoch-progress">
|
<div class="item">
|
||||||
<svg class="epoch-blocks" height="22px" width="100%" viewBox="0 0 224 9" shape-rendering="crispEdges" preserveAspectRatio="none">
|
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
|
||||||
<defs>
|
<div class="card-text">
|
||||||
<linearGradient id="diff-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
|
<ng-container *ngTemplateOutlet="epochData.remainingBlocks === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.remainingBlocks }"></ng-container>
|
||||||
<stop offset="0%" stop-color="#105fb0" />
|
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||||
<stop offset="100%" stop-color="#9339f4" />
|
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
||||||
</linearGradient>
|
</div>
|
||||||
<linearGradient id="diff-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%" gradientUnits="userSpaceOnUse">
|
<div class="symbol"><app-time-until [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time-until></div>
|
||||||
<stop offset="0%" stop-color="#2486eb" />
|
|
||||||
<stop offset="100%" stop-color="#ae6af7" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect
|
|
||||||
*ngFor="let rect of shapes"
|
|
||||||
[attr.x]="rect.x" [attr.y]="rect.y"
|
|
||||||
[attr.width]="rect.w" [attr.height]="rect.h"
|
|
||||||
class="rect {{rect.status}}"
|
|
||||||
[class.hover]="hoverSection && rect.status === hoverSection.status"
|
|
||||||
(pointerover)="onHover($event, rect);"
|
|
||||||
(pointerout)="onBlur($event);"
|
|
||||||
>
|
|
||||||
<animate
|
|
||||||
*ngIf="rect.status === 'next'"
|
|
||||||
attributeType="XML"
|
|
||||||
attributeName="fill"
|
|
||||||
[attr.values]="'#fff;' + (rect.expected ? '#D81B60' : '#2d3348') + ';#fff'"
|
|
||||||
dur="2s"
|
|
||||||
repeatCount="indefinite"/>
|
|
||||||
</rect>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="difficulty-stats">
|
<div class="item">
|
||||||
<div class="item">
|
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
|
||||||
<div class="card-text">
|
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
|
||||||
~<app-time [time]="epochData.timeAvg / 1000" [forceFloorOnTimeIntervals]="['minute']" [fractionDigits]="1"></app-time>
|
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
|
||||||
</div>
|
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||||
<div class="symbol" i18n="difficulty-box.average-block-time">Average block time</div>
|
</span>
|
||||||
</div>
|
<ng-template #arrowDownDifficulty >
|
||||||
<div class="item">
|
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
||||||
<div *ngIf="epochData.remainingBlocks < 1870; else recentlyAdjusted" class="card-text" [ngStyle]="{'color': epochData.colorAdjustments}">
|
|
||||||
<span *ngIf="epochData.change > 0; else arrowDownDifficulty" >
|
|
||||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
|
||||||
</span>
|
|
||||||
<ng-template #arrowDownDifficulty >
|
|
||||||
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
|
||||||
</ng-template>
|
|
||||||
{{ epochData.change | absolute | number: '1.2-2' }}
|
|
||||||
<span class="symbol">%</span>
|
|
||||||
</div>
|
|
||||||
<ng-template #recentlyAdjusted>
|
|
||||||
<div class="card-text">—</div>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<div class="symbol">
|
{{ epochData.change | absolute | number: '1.2-2' }}
|
||||||
<span i18n="difficulty-box.previous">Previous</span>:
|
<span class="symbol">%</span>
|
||||||
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
|
|
||||||
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
|
|
||||||
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
|
||||||
</span>
|
|
||||||
<ng-template #arrowDownPreviousDifficulty >
|
|
||||||
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
|
||||||
</ng-template>
|
|
||||||
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> %
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<ng-template #recentlyAdjusted>
|
||||||
<div class="card-text"><app-time kind="until" [time]="epochData.estimatedRetargetDate" [fastRender]="true"></app-time></div>
|
<div class="card-text">—</div>
|
||||||
<div class="symbol">
|
</ng-template>
|
||||||
{{ epochData.retargetDateString }}
|
<div class="symbol">
|
||||||
</div>
|
<span i18n="difficulty-box.previous">Previous</span>:
|
||||||
|
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
|
||||||
|
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
|
||||||
|
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</span>
|
||||||
|
<ng-template #arrowDownPreviousDifficulty >
|
||||||
|
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
|
||||||
|
</ng-template>
|
||||||
|
{{ epochData.previousRetarget | absolute | number: '1.2-2' }} </span> %
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="item" *ngIf="showProgress">
|
||||||
|
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
|
||||||
|
<div class="card-text">{{ epochData.progress | number: '1.2-2' }} <span class="symbol">%</span></div>
|
||||||
|
<div class="progress small-bar">
|
||||||
|
<div class="progress-bar" role="progressbar" style="width: 15%; background-color: #105fb0" [ngStyle]="{'width': epochData.base}"> </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item" *ngIf="showHalving">
|
||||||
|
<h5 class="card-title" i18n="difficulty-box.next-halving">Next Halving</h5>
|
||||||
|
<div class="card-text">
|
||||||
|
<ng-container *ngTemplateOutlet="epochData.blocksUntilHalving === 1 ? blocksSingular : blocksPlural; context: {$implicit: epochData.blocksUntilHalving }"></ng-container>
|
||||||
|
<ng-template #blocksPlural let-i i18n="shared.blocks">{{ i }} <span class="shared-block">blocks</span></ng-template>
|
||||||
|
<ng-template #blocksSingular let-i i18n="shared.block">{{ i }} <span class="shared-block">block</span></ng-template>
|
||||||
|
</div>
|
||||||
|
<div class="symbol"><app-time-until [time]="epochData.timeUntilHalving" [fastRender]="true"></app-time-until></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #loadingDifficulty>
|
<ng-template #loadingDifficulty>
|
||||||
<div class="epoch-progress">
|
|
||||||
<div class="skeleton-loader"></div>
|
|
||||||
</div>
|
|
||||||
<div class="difficulty-skeleton loading-container">
|
<div class="difficulty-skeleton loading-container">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
|
<h5 class="card-title" i18n="difficulty-box.remaining">Remaining</h5>
|
||||||
<div class="card-text">
|
<div class="card-text">
|
||||||
<div class="skeleton-loader"></div>
|
<div class="skeleton-loader"></div>
|
||||||
<div class="skeleton-loader"></div>
|
<div class="skeleton-loader"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
|
<h5 class="card-title" i18n="difficulty-box.estimate">Estimate</h5>
|
||||||
<div class="card-text">
|
<div class="card-text">
|
||||||
<div class="skeleton-loader"></div>
|
<div class="skeleton-loader"></div>
|
||||||
<div class="skeleton-loader"></div>
|
<div class="skeleton-loader"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
|
<h5 class="card-title" i18n="difficulty-box.current-period">Current Period</h5>
|
||||||
<div class="card-text">
|
<div class="card-text">
|
||||||
<div class="skeleton-loader"></div>
|
<div class="skeleton-loader"></div>
|
||||||
<div class="skeleton-loader"></div>
|
<div class="skeleton-loader"></div>
|
||||||
@@ -104,10 +85,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<app-difficulty-tooltip
|
|
||||||
*ngIf="hoverSection && (isLoadingWebSocket$ | async) === false && (difficultyEpoch$ | async) as epochData"
|
|
||||||
[cursorPosition]="tooltipPosition"
|
|
||||||
[status]="hoverSection.status"
|
|
||||||
[progress]="epochData"
|
|
||||||
></app-difficulty-tooltip>
|
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
.difficulty-adjustment-container {
|
.difficulty-adjustment-container {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.difficulty-stats {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
height: 50.5px;
|
height: 76px;
|
||||||
.shared-block {
|
.shared-block {
|
||||||
color: #ffffff66;
|
color: #ffffff66;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -30,8 +24,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.card-text {
|
.card-text {
|
||||||
font-size: 20px;
|
font-size: 22px;
|
||||||
margin: auto;
|
margin-top: -9px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,14 +33,11 @@
|
|||||||
|
|
||||||
.difficulty-skeleton {
|
.difficulty-skeleton {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
justify-content: space-between;
|
||||||
justify-content: space-around;
|
|
||||||
height: 50.5px;
|
|
||||||
@media (min-width: 376px) {
|
@media (min-width: 376px) {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
.item {
|
.item {
|
||||||
min-width: 120px;
|
|
||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: -webkit-fill-available;
|
width: -webkit-fill-available;
|
||||||
@@ -74,7 +65,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin: 10px auto 4px;
|
margin: 14px auto 0;
|
||||||
max-width: 80px;
|
max-width: 80px;
|
||||||
}
|
}
|
||||||
&:last-child {
|
&:last-child {
|
||||||
@@ -118,7 +109,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.loading-container {
|
.loading-container {
|
||||||
min-height: 50.5px;
|
min-height: 76px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-title {
|
.main-title {
|
||||||
@@ -142,7 +133,7 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
padding: 20px;
|
padding: 24px 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,50 +151,4 @@
|
|||||||
|
|
||||||
.symbol {
|
.symbol {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
|
||||||
|
|
||||||
.epoch-progress {
|
|
||||||
width: 100%;
|
|
||||||
height: 22px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.epoch-blocks {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
background: #2d3348;
|
|
||||||
|
|
||||||
.rect {
|
|
||||||
fill: #2d3348;
|
|
||||||
|
|
||||||
&.behind {
|
|
||||||
fill: #D81B60;
|
|
||||||
}
|
|
||||||
&.mined {
|
|
||||||
fill: url(#diff-gradient);
|
|
||||||
}
|
|
||||||
&.ahead {
|
|
||||||
fill: #1a9436;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.hover {
|
|
||||||
fill: #535e84;
|
|
||||||
&.behind {
|
|
||||||
fill: #e94d86;
|
|
||||||
}
|
|
||||||
&.mined {
|
|
||||||
fill: url(#diff-hover-gradient);
|
|
||||||
}
|
|
||||||
&.ahead {
|
|
||||||
fill: #29d951;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.blocks-ahead {
|
|
||||||
color: #3bcc49;
|
|
||||||
}
|
|
||||||
.blocks-behind {
|
|
||||||
color: #D81B60;
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectionStrategy, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||||
import { combineLatest, Observable, timer } from 'rxjs';
|
import { combineLatest, Observable, timer } from 'rxjs';
|
||||||
import { map, switchMap } from 'rxjs/operators';
|
import { map, switchMap } from 'rxjs/operators';
|
||||||
import { StateService } from '../..//services/state.service';
|
import { StateService } from '../..//services/state.service';
|
||||||
@@ -7,33 +7,16 @@ interface EpochProgress {
|
|||||||
base: string;
|
base: string;
|
||||||
change: number;
|
change: number;
|
||||||
progress: number;
|
progress: number;
|
||||||
minedBlocks: number;
|
|
||||||
remainingBlocks: number;
|
remainingBlocks: number;
|
||||||
expectedBlocks: number;
|
|
||||||
newDifficultyHeight: number;
|
newDifficultyHeight: number;
|
||||||
colorAdjustments: string;
|
colorAdjustments: string;
|
||||||
colorPreviousAdjustments: string;
|
colorPreviousAdjustments: string;
|
||||||
estimatedRetargetDate: number;
|
estimatedRetargetDate: number;
|
||||||
retargetDateString: string;
|
|
||||||
previousRetarget: number;
|
previousRetarget: number;
|
||||||
blocksUntilHalving: number;
|
blocksUntilHalving: number;
|
||||||
timeUntilHalving: number;
|
timeUntilHalving: number;
|
||||||
timeAvg: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BlockStatus = 'mined' | 'behind' | 'ahead' | 'next' | 'remaining';
|
|
||||||
|
|
||||||
interface DiffShape {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
w: number;
|
|
||||||
h: number;
|
|
||||||
status: BlockStatus;
|
|
||||||
expected: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-difficulty',
|
selector: 'app-difficulty',
|
||||||
templateUrl: './difficulty.component.html',
|
templateUrl: './difficulty.component.html',
|
||||||
@@ -41,27 +24,15 @@ const EPOCH_BLOCK_LENGTH = 2016; // Bitcoin mainnet
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class DifficultyComponent implements OnInit {
|
export class DifficultyComponent implements OnInit {
|
||||||
@Input() showProgress = true;
|
|
||||||
@Input() showHalving = false;
|
|
||||||
@Input() showTitle = true;
|
|
||||||
|
|
||||||
isLoadingWebSocket$: Observable<boolean>;
|
isLoadingWebSocket$: Observable<boolean>;
|
||||||
difficultyEpoch$: Observable<EpochProgress>;
|
difficultyEpoch$: Observable<EpochProgress>;
|
||||||
|
|
||||||
epochStart: number;
|
@Input() showProgress = true;
|
||||||
currentHeight: number;
|
@Input() showHalving = false;
|
||||||
currentIndex: number;
|
@Input() showTitle = true;
|
||||||
expectedHeight: number;
|
|
||||||
expectedIndex: number;
|
|
||||||
difference: number;
|
|
||||||
shapes: DiffShape[];
|
|
||||||
|
|
||||||
tooltipPosition = { x: 0, y: 0 };
|
|
||||||
hoverSection: DiffShape | void;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
@Inject(LOCALE_ID) private locale: string,
|
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -94,110 +65,22 @@ export class DifficultyComponent implements OnInit {
|
|||||||
|
|
||||||
const blocksUntilHalving = 210000 - (block.height % 210000);
|
const blocksUntilHalving = 210000 - (block.height % 210000);
|
||||||
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
|
const timeUntilHalving = new Date().getTime() + (blocksUntilHalving * 600000);
|
||||||
const newEpochStart = Math.floor(this.stateService.latestBlockHeight / EPOCH_BLOCK_LENGTH) * EPOCH_BLOCK_LENGTH;
|
|
||||||
const newExpectedHeight = Math.floor(newEpochStart + da.expectedBlocks);
|
|
||||||
|
|
||||||
if (newEpochStart !== this.epochStart || newExpectedHeight !== this.expectedHeight || this.currentHeight !== this.stateService.latestBlockHeight) {
|
|
||||||
this.epochStart = newEpochStart;
|
|
||||||
this.expectedHeight = newExpectedHeight;
|
|
||||||
this.currentHeight = this.stateService.latestBlockHeight;
|
|
||||||
this.currentIndex = this.currentHeight - this.epochStart;
|
|
||||||
this.expectedIndex = Math.min(this.expectedHeight - this.epochStart, 2016) - 1;
|
|
||||||
this.difference = this.currentIndex - this.expectedIndex;
|
|
||||||
|
|
||||||
this.shapes = [];
|
|
||||||
this.shapes = this.shapes.concat(this.blocksToShapes(
|
|
||||||
0, Math.min(this.currentIndex, this.expectedIndex), 'mined', true
|
|
||||||
));
|
|
||||||
this.shapes = this.shapes.concat(this.blocksToShapes(
|
|
||||||
this.currentIndex + 1, this.expectedIndex, 'behind', true
|
|
||||||
));
|
|
||||||
this.shapes = this.shapes.concat(this.blocksToShapes(
|
|
||||||
this.expectedIndex + 1, this.currentIndex, 'ahead', false
|
|
||||||
));
|
|
||||||
if (this.currentIndex < 2015) {
|
|
||||||
this.shapes = this.shapes.concat(this.blocksToShapes(
|
|
||||||
this.currentIndex + 1, this.currentIndex + 1, 'next', (this.expectedIndex > this.currentIndex)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
this.shapes = this.shapes.concat(this.blocksToShapes(
|
|
||||||
Math.max(this.currentIndex + 2, this.expectedIndex + 1), 2105, 'remaining', false
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let retargetDateString;
|
|
||||||
if (da.remainingBlocks > 1870) {
|
|
||||||
retargetDateString = (new Date(da.estimatedRetargetDate)).toLocaleDateString(this.locale, { month: 'long', day: 'numeric' });
|
|
||||||
} else {
|
|
||||||
retargetDateString = (new Date(da.estimatedRetargetDate)).toLocaleTimeString(this.locale, { month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
base: `${da.progressPercent.toFixed(2)}%`,
|
base: `${da.progressPercent.toFixed(2)}%`,
|
||||||
change: da.difficultyChange,
|
change: da.difficultyChange,
|
||||||
progress: da.progressPercent,
|
progress: da.progressPercent,
|
||||||
minedBlocks: this.currentIndex + 1,
|
remainingBlocks: da.remainingBlocks,
|
||||||
remainingBlocks: da.remainingBlocks - 1,
|
|
||||||
expectedBlocks: Math.floor(da.expectedBlocks),
|
|
||||||
colorAdjustments,
|
colorAdjustments,
|
||||||
colorPreviousAdjustments,
|
colorPreviousAdjustments,
|
||||||
newDifficultyHeight: da.nextRetargetHeight,
|
newDifficultyHeight: da.nextRetargetHeight,
|
||||||
estimatedRetargetDate: da.estimatedRetargetDate,
|
estimatedRetargetDate: da.estimatedRetargetDate,
|
||||||
retargetDateString,
|
|
||||||
previousRetarget: da.previousRetarget,
|
previousRetarget: da.previousRetarget,
|
||||||
blocksUntilHalving,
|
blocksUntilHalving,
|
||||||
timeUntilHalving,
|
timeUntilHalving,
|
||||||
timeAvg: da.timeAvg,
|
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
blocksToShapes(start: number, end: number, status: BlockStatus, expected: boolean = false): DiffShape[] {
|
|
||||||
const startY = start % 9;
|
|
||||||
const startX = Math.floor(start / 9);
|
|
||||||
const endY = (end % 9);
|
|
||||||
const endX = Math.floor(end / 9);
|
|
||||||
|
|
||||||
if (startX > endX) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startX === endX) {
|
|
||||||
return [{
|
|
||||||
x: startX, y: startY, w: 1, h: 1 + endY - startY, status, expected
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
const shapes = [];
|
|
||||||
shapes.push({
|
|
||||||
x: startX, y: startY, w: 1, h: 9 - startY, status, expected
|
|
||||||
});
|
|
||||||
shapes.push({
|
|
||||||
x: endX, y: 0, w: 1, h: endY + 1, status, expected
|
|
||||||
});
|
|
||||||
|
|
||||||
if (startX < endX - 1) {
|
|
||||||
shapes.push({
|
|
||||||
x: startX + 1, y: 0, w: endX - startX - 1, h: 9, status, expected
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return shapes;
|
|
||||||
}
|
|
||||||
|
|
||||||
@HostListener('pointermove', ['$event'])
|
|
||||||
onPointerMove(event) {
|
|
||||||
this.tooltipPosition = { x: event.clientX, y: event.clientY };
|
|
||||||
}
|
|
||||||
|
|
||||||
onHover(event, rect): void {
|
|
||||||
this.hoverSection = rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
onBlur(event): void {
|
|
||||||
this.hoverSection = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<div *ngIf="stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING" class="mb-3 d-flex menu">
|
<div *ngIf="stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING" class="mb-3 d-flex menu"
|
||||||
|
style="padding: 0px 35px;">
|
||||||
|
|
||||||
<a routerLinkActive="active" class="btn btn-primary" [class]="padding"
|
<a routerLinkActive="active" class="btn btn-primary mr-1" [class]="padding"
|
||||||
[routerLink]="['/graphs/mempool' | relativeUrl]">Mempool</a>
|
[routerLink]="['/graphs/mempool' | relativeUrl]">Mempool</a>
|
||||||
|
|
||||||
<div ngbDropdown [class]="padding" *ngIf="stateService.env.MINING_DASHBOARD">
|
<div ngbDropdown class="mr-1" [class]="padding" *ngIf="stateService.env.MINING_DASHBOARD">
|
||||||
<button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="mining">Mining</button>
|
<button class="btn btn-primary w-100" id="dropdownBasic1" ngbDropdownToggle i18n="mining">Mining</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
||||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools' | relativeUrl]"
|
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/mining/pools' | relativeUrl]"
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
.menu {
|
.menu {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 0 35px;
|
|
||||||
@media (min-width: 576px) {
|
@media (min-width: 576px) {
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
& > * {
|
|
||||||
margin: 0;
|
|
||||||
margin-inline-end: 0.25rem;
|
|
||||||
&.last-child {
|
|
||||||
margin-inline-end: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -54,6 +54,31 @@
|
|||||||
height: 240px;
|
height: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.formRadioGroup {
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
@media (min-width: 991px) {
|
||||||
|
position: relative;
|
||||||
|
top: -100px;
|
||||||
|
}
|
||||||
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
|
position: relative;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
@media (min-width: 830px) {
|
||||||
|
flex-direction: row;
|
||||||
|
float: right;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 9px;
|
||||||
|
@media (min-width: 830px) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.pool-distribution {
|
.pool-distribution {
|
||||||
min-height: 56px;
|
min-height: 56px;
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||||
import { EChartsOption, graphic } from 'echarts';
|
import { EChartsOption, graphic } from 'echarts';
|
||||||
import { merge, Observable, of } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map, mergeMap, share, startWith, switchMap, tap } from 'rxjs/operators';
|
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { formatNumber } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
@@ -84,84 +84,77 @@ export class HashrateChartComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.hashrateObservable$ = merge(
|
this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
|
||||||
this.radioGroupForm.get('dateSpan').valueChanges
|
.pipe(
|
||||||
.pipe(
|
startWith(this.radioGroupForm.controls.dateSpan.value),
|
||||||
startWith(this.radioGroupForm.controls.dateSpan.value),
|
switchMap((timespan) => {
|
||||||
switchMap((timespan) => {
|
if (!this.widget && !firstRun) {
|
||||||
if (!this.widget && !firstRun) {
|
this.storageService.setValue('miningWindowPreference', timespan);
|
||||||
this.storageService.setValue('miningWindowPreference', timespan);
|
}
|
||||||
}
|
this.timespan = timespan;
|
||||||
this.timespan = timespan;
|
firstRun = false;
|
||||||
firstRun = false;
|
this.miningWindowPreference = timespan;
|
||||||
this.miningWindowPreference = timespan;
|
this.isLoading = true;
|
||||||
this.isLoading = true;
|
return this.apiService.getHistoricalHashrate$(timespan)
|
||||||
return this.apiService.getHistoricalHashrate$(this.timespan);
|
.pipe(
|
||||||
})
|
tap((response) => {
|
||||||
),
|
const data = response.body;
|
||||||
this.stateService.chainTip$
|
|
||||||
.pipe(
|
|
||||||
switchMap(() => {
|
|
||||||
return this.apiService.getHistoricalHashrate$(this.timespan);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
).pipe(
|
|
||||||
tap((response: any) => {
|
|
||||||
const data = response.body;
|
|
||||||
|
|
||||||
// We generate duplicated data point so the tooltip works nicely
|
// We generate duplicated data point so the tooltip works nicely
|
||||||
const diffFixed = [];
|
const diffFixed = [];
|
||||||
let diffIndex = 1;
|
let diffIndex = 1;
|
||||||
let hashIndex = 0;
|
let hashIndex = 0;
|
||||||
while (hashIndex < data.hashrates.length) {
|
while (hashIndex < data.hashrates.length) {
|
||||||
if (diffIndex >= data.difficulty.length) {
|
if (diffIndex >= data.difficulty.length) {
|
||||||
while (hashIndex < data.hashrates.length) {
|
while (hashIndex < data.hashrates.length) {
|
||||||
diffFixed.push({
|
diffFixed.push({
|
||||||
timestamp: data.hashrates[hashIndex].timestamp,
|
timestamp: data.hashrates[hashIndex].timestamp,
|
||||||
difficulty: data.difficulty.length > 0 ? data.difficulty[data.difficulty.length - 1].difficulty : null
|
difficulty: data.difficulty.length > 0 ? data.difficulty[data.difficulty.length - 1].difficulty : null
|
||||||
|
});
|
||||||
|
++hashIndex;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (hashIndex < data.hashrates.length && diffIndex < data.difficulty.length &&
|
||||||
|
data.hashrates[hashIndex].timestamp <= data.difficulty[diffIndex].time
|
||||||
|
) {
|
||||||
|
diffFixed.push({
|
||||||
|
timestamp: data.hashrates[hashIndex].timestamp,
|
||||||
|
difficulty: data.difficulty[diffIndex - 1].difficulty
|
||||||
|
});
|
||||||
|
++hashIndex;
|
||||||
|
}
|
||||||
|
++diffIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
let maResolution = 15;
|
||||||
|
const hashrateMa = [];
|
||||||
|
for (let i = maResolution - 1; i < data.hashrates.length; ++i) {
|
||||||
|
let avg = 0;
|
||||||
|
for (let y = maResolution - 1; y >= 0; --y) {
|
||||||
|
avg += data.hashrates[i - y].avgHashrate;
|
||||||
|
}
|
||||||
|
avg /= maResolution;
|
||||||
|
hashrateMa.push([data.hashrates[i].timestamp * 1000, avg]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prepareChartOptions({
|
||||||
|
hashrates: data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]),
|
||||||
|
difficulty: diffFixed.map(val => [val.timestamp * 1000, val.difficulty]),
|
||||||
|
hashrateMa: hashrateMa,
|
||||||
});
|
});
|
||||||
++hashIndex;
|
this.isLoading = false;
|
||||||
}
|
}),
|
||||||
break;
|
map((response) => {
|
||||||
}
|
const data = response.body;
|
||||||
|
return {
|
||||||
while (hashIndex < data.hashrates.length && diffIndex < data.difficulty.length &&
|
blockCount: parseInt(response.headers.get('x-total-count'), 10),
|
||||||
data.hashrates[hashIndex].timestamp <= data.difficulty[diffIndex].time
|
currentDifficulty: data.currentDifficulty,
|
||||||
) {
|
currentHashrate: data.currentHashrate,
|
||||||
diffFixed.push({
|
};
|
||||||
timestamp: data.hashrates[hashIndex].timestamp,
|
}),
|
||||||
difficulty: data.difficulty[diffIndex - 1].difficulty
|
);
|
||||||
});
|
|
||||||
++hashIndex;
|
|
||||||
}
|
|
||||||
++diffIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
let maResolution = 15;
|
|
||||||
const hashrateMa = [];
|
|
||||||
for (let i = maResolution - 1; i < data.hashrates.length; ++i) {
|
|
||||||
let avg = 0;
|
|
||||||
for (let y = maResolution - 1; y >= 0; --y) {
|
|
||||||
avg += data.hashrates[i - y].avgHashrate;
|
|
||||||
}
|
|
||||||
avg /= maResolution;
|
|
||||||
hashrateMa.push([data.hashrates[i].timestamp * 1000, avg]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.prepareChartOptions({
|
|
||||||
hashrates: data.hashrates.map(val => [val.timestamp * 1000, val.avgHashrate]),
|
|
||||||
difficulty: diffFixed.map(val => [val.timestamp * 1000, val.difficulty]),
|
|
||||||
hashrateMa: hashrateMa,
|
|
||||||
});
|
|
||||||
this.isLoading = false;
|
|
||||||
}),
|
|
||||||
map((response) => {
|
|
||||||
const data = response.body;
|
|
||||||
return {
|
|
||||||
blockCount: parseInt(response.headers.get('x-total-count'), 10),
|
|
||||||
currentDifficulty: data.currentDifficulty,
|
|
||||||
currentHashrate: data.currentHashrate,
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
share()
|
share()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,6 +48,31 @@
|
|||||||
max-height: 293px;
|
max-height: 293px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.formRadioGroup {
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
@media (min-width: 991px) {
|
||||||
|
position: relative;
|
||||||
|
top: -100px;
|
||||||
|
}
|
||||||
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
|
position: relative;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
@media (min-width: 830px) {
|
||||||
|
flex-direction: row;
|
||||||
|
float: right;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 9px;
|
||||||
|
@media (min-width: 830px) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.loadingGraphs {
|
.loadingGraphs {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(difficultyAdjustments$ | async) as da;">
|
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(difficultyAdjustments$ | async) as da;">
|
||||||
<div class="flashing">
|
<div class="flashing">
|
||||||
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
|
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
|
||||||
<div @blockEntryTrigger [@.disabled]="!animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
|
<div [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
|
||||||
<a draggable="false" [routerLink]="['/mempool-block/' | relativeUrl, i]"
|
<a draggable="false" [routerLink]="['/mempool-block/' | relativeUrl, i]"
|
||||||
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
||||||
<div class="block-body">
|
<div class="block-body">
|
||||||
@@ -23,10 +23,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div [attr.data-cy]="'mempool-block-' + i + '-time'" class="time-difference" *ngIf="projectedBlock.blockVSize <= stateService.blockVSize; else mergedBlock">
|
<div [attr.data-cy]="'mempool-block-' + i + '-time'" class="time-difference" *ngIf="projectedBlock.blockVSize <= stateService.blockVSize; else mergedBlock">
|
||||||
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeDiffMainnet">
|
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeDiffMainnet">
|
||||||
<app-time kind="until" [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time>
|
<app-time-until [time]="(1 * i) + now + 61000" [fastRender]="false" [fixedRender]="true"></app-time-until>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #timeDiffMainnet>
|
<ng-template #timeDiffMainnet>
|
||||||
<app-time kind="until" [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time>
|
<app-time-until [time]="da.timeAvg * (i + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time-until>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
<ng-template #mergedBlock>
|
<ng-template #mergedBlock>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input } from '@angular/core';
|
||||||
import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
|
import { Subscription, Observable, fromEvent, merge, of, combineLatest, timer } from 'rxjs';
|
||||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
@@ -9,18 +9,11 @@ import { specialBlocks } from '../../app.constants';
|
|||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { DifficultyAdjustment } from '../../interfaces/node-api.interface';
|
import { DifficultyAdjustment } from '../../interfaces/node-api.interface';
|
||||||
import { animate, style, transition, trigger } from '@angular/animations';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-mempool-blocks',
|
selector: 'app-mempool-blocks',
|
||||||
templateUrl: './mempool-blocks.component.html',
|
templateUrl: './mempool-blocks.component.html',
|
||||||
styleUrls: ['./mempool-blocks.component.scss'],
|
styleUrls: ['./mempool-blocks.component.scss'],
|
||||||
animations: [trigger('blockEntryTrigger', [
|
|
||||||
transition(':enter', [
|
|
||||||
style({ transform: 'translateX(-155px)' }),
|
|
||||||
animate('2s 0s ease', style({ transform: '' })),
|
|
||||||
]),
|
|
||||||
])],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
||||||
@@ -39,14 +32,12 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
isLoadingWebsocketSubscription: Subscription;
|
isLoadingWebsocketSubscription: Subscription;
|
||||||
blockSubscription: Subscription;
|
blockSubscription: Subscription;
|
||||||
networkSubscription: Subscription;
|
networkSubscription: Subscription;
|
||||||
chainTipSubscription: Subscription;
|
|
||||||
network = '';
|
network = '';
|
||||||
now = new Date().getTime();
|
now = new Date().getTime();
|
||||||
timeOffset = 0;
|
timeOffset = 0;
|
||||||
showMiningInfo = false;
|
showMiningInfo = false;
|
||||||
timeLtrSubscription: Subscription;
|
timeLtrSubscription: Subscription;
|
||||||
timeLtr: boolean;
|
timeLtr: boolean;
|
||||||
animateEntry: boolean = false;
|
|
||||||
|
|
||||||
blockWidth = 125;
|
blockWidth = 125;
|
||||||
blockPadding = 30;
|
blockPadding = 30;
|
||||||
@@ -62,7 +53,6 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
resetTransitionTimeout: number;
|
resetTransitionTimeout: number;
|
||||||
|
|
||||||
chainTip: number = -1;
|
|
||||||
blockIndex = 1;
|
blockIndex = 1;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -79,8 +69,6 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.chainTip = this.stateService.latestBlockHeight;
|
|
||||||
|
|
||||||
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
||||||
this.enabledMiningInfoIfNeeded(this.location.path());
|
this.enabledMiningInfoIfNeeded(this.location.path());
|
||||||
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
|
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
|
||||||
@@ -128,7 +116,9 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
mempoolBlocks.forEach((block, i) => {
|
mempoolBlocks.forEach((block, i) => {
|
||||||
block.index = this.blockIndex + i;
|
block.index = this.blockIndex + i;
|
||||||
block.height = lastBlock.height + i + 1;
|
block.height = lastBlock.height + i + 1;
|
||||||
block.blink = specialBlocks[block.height]?.networks.includes(this.stateService.network || 'mainnet') ? true : false;
|
if (this.stateService.network === '') {
|
||||||
|
block.blink = specialBlocks[block.height] ? true : false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const stringifiedBlocks = JSON.stringify(mempoolBlocks);
|
const stringifiedBlocks = JSON.stringify(mempoolBlocks);
|
||||||
@@ -165,24 +155,11 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.blockSubscription = this.stateService.blocks$
|
this.blockSubscription = this.stateService.blocks$
|
||||||
.subscribe(([block]) => {
|
.subscribe(([block]) => {
|
||||||
if (this.chainTip === -1) {
|
if (block?.extras?.matchRate >= 66 && !this.tabHidden) {
|
||||||
this.animateEntry = block.height === this.stateService.latestBlockHeight;
|
|
||||||
} else {
|
|
||||||
this.animateEntry = block.height > this.chainTip;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.chainTip = this.stateService.latestBlockHeight;
|
|
||||||
if ((block?.extras?.similarity == null || block?.extras?.similarity > 0.5) && !this.tabHidden) {
|
|
||||||
this.blockIndex++;
|
this.blockIndex++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.chainTipSubscription = this.stateService.chainTip$.subscribe((height) => {
|
|
||||||
if (this.chainTip === -1) {
|
|
||||||
this.chainTip = height;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.networkSubscription = this.stateService.networkChanged$
|
this.networkSubscription = this.stateService.networkChanged$
|
||||||
.subscribe((network) => this.network = network);
|
.subscribe((network) => this.network = network);
|
||||||
|
|
||||||
@@ -218,12 +195,11 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
this.blockSubscription.unsubscribe();
|
this.blockSubscription.unsubscribe();
|
||||||
this.networkSubscription.unsubscribe();
|
this.networkSubscription.unsubscribe();
|
||||||
this.timeLtrSubscription.unsubscribe();
|
this.timeLtrSubscription.unsubscribe();
|
||||||
this.chainTipSubscription.unsubscribe();
|
|
||||||
clearTimeout(this.resetTransitionTimeout);
|
clearTimeout(this.resetTransitionTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackByFn(index: number, block: MempoolBlock) {
|
trackByFn(index: number, block: MempoolBlock) {
|
||||||
return (block.isStack) ? 'stack' : block.index;
|
return block.index;
|
||||||
}
|
}
|
||||||
|
|
||||||
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
||||||
@@ -238,10 +214,6 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
lastBlock.feeRange = lastBlock.feeRange.concat(block.feeRange);
|
lastBlock.feeRange = lastBlock.feeRange.concat(block.feeRange);
|
||||||
lastBlock.feeRange.sort((a, b) => a - b);
|
lastBlock.feeRange.sort((a, b) => a - b);
|
||||||
lastBlock.medianFee = this.median(lastBlock.feeRange);
|
lastBlock.medianFee = this.median(lastBlock.feeRange);
|
||||||
lastBlock.totalFees += block.totalFees;
|
|
||||||
}
|
|
||||||
if (blocks.length) {
|
|
||||||
blocks[blocks.length - 1].isStack = blocks[blocks.length - 1].blockVSize > this.stateService.blockVSize;
|
|
||||||
}
|
}
|
||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
@@ -361,4 +333,4 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
return emptyBlocks;
|
return emptyBlocks;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
<div class="row row-cols-1 row-cols-md-2">
|
<div class="row row-cols-1 row-cols-md-2">
|
||||||
|
|
||||||
|
<!-- Temporary stuff here - Will be moved to a component once we have more useful data to show -->
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="main-title">
|
<div class="main-title">
|
||||||
<span [attr.data-cy]="'reward-stats'" i18n="mining.reward-stats">Reward stats</span>
|
<span [attr.data-cy]="'reward-stats'" i18n="mining.reward-stats">Reward stats</span>
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
<!-- difficulty adjustment -->
|
<!-- difficulty adjustment -->
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
|
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
|
||||||
<app-difficulty-mining [attr.data-cy]="'difficulty-adjustment'" [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty-mining>
|
<app-difficulty [attr.data-cy]="'difficulty-adjustment'" [showTitle]="false" [showProgress]="false" [showHalving]="true"></app-difficulty>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- pool distribution -->
|
<!-- pool distribution -->
|
||||||
|
|||||||
@@ -33,6 +33,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.formRadioGroup {
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
@media (min-width: 991px) {
|
||||||
|
position: relative;
|
||||||
|
top: -100px;
|
||||||
|
}
|
||||||
|
@media (min-width: 830px) and (max-width: 991px) {
|
||||||
|
position: relative;
|
||||||
|
top: 0px;
|
||||||
|
}
|
||||||
|
@media (min-width: 830px) {
|
||||||
|
flex-direction: row;
|
||||||
|
float: right;
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
.btn-sm {
|
||||||
|
font-size: 9px;
|
||||||
|
@media (min-width: 830px) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bottom-padding {
|
.bottom-padding {
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
padding-bottom: 65px
|
padding-bottom: 65px
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding
|
|||||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||||
import { merge, Observable } from 'rxjs';
|
import { concat, Observable } from 'rxjs';
|
||||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { StorageService } from '../..//services/storage.service';
|
import { StorageService } from '../..//services/storage.service';
|
||||||
@@ -73,7 +73,7 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.miningStatsObservable$ = merge(
|
this.miningStatsObservable$ = concat(
|
||||||
this.radioGroupForm.get('dateSpan').valueChanges
|
this.radioGroupForm.get('dateSpan').valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
startWith(this.radioGroupForm.controls.dateSpan.value), // (trigger when the page loads)
|
startWith(this.radioGroupForm.controls.dateSpan.value), // (trigger when the page loads)
|
||||||
@@ -89,7 +89,7 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
return this.miningService.getMiningStats(this.miningWindowPreference);
|
return this.miningService.getMiningStats(this.miningWindowPreference);
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
this.stateService.chainTip$
|
this.stateService.blocks$
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
return this.miningService.getMiningStats(this.miningWindowPreference);
|
return this.miningService.getMiningStats(this.miningWindowPreference);
|
||||||
@@ -162,10 +162,10 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
if (this.miningWindowPreference === '24h') {
|
if (this.miningWindowPreference === '24h') {
|
||||||
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
||||||
pool.lastEstimatedHashrate.toString() + ' PH/s' +
|
pool.lastEstimatedHashrate.toString() + ' PH/s' +
|
||||||
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
`<br>` + $localize`${i} blocks`;
|
||||||
} else {
|
} else {
|
||||||
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
|
||||||
$localize`${ i }:INTERPOLATION: blocks`;
|
$localize`${i} blocks`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -195,15 +195,13 @@ export class PoolRankingComponent implements OnInit {
|
|||||||
},
|
},
|
||||||
borderColor: '#000',
|
borderColor: '#000',
|
||||||
formatter: () => {
|
formatter: () => {
|
||||||
const percentage = totalShareOther.toFixed(2) + '%';
|
|
||||||
const i = totalBlockOther.toString();
|
|
||||||
if (this.miningWindowPreference === '24h') {
|
if (this.miningWindowPreference === '24h') {
|
||||||
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
|
return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
|
||||||
totalEstimatedHashrateOther.toString() + ' PH/s' +
|
totalEstimatedHashrateOther.toString() + ' PH/s' +
|
||||||
`<br>` + $localize`${ i }:INTERPOLATION: blocks`;
|
`<br>` + totalBlockOther.toString() + ` blocks`;
|
||||||
} else {
|
} else {
|
||||||
return `<b style="color: white">` + $localize`Other (${percentage})` + `</b><br>` +
|
return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
|
||||||
$localize`${ i }:INTERPOLATION: blocks`;
|
totalBlockOther.toString() + ` blocks`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -86,6 +86,11 @@ export class PoolPreviewComponent implements OnInit {
|
|||||||
regexes += regex + '", "';
|
regexes += regex + '", "';
|
||||||
}
|
}
|
||||||
poolStats.pool.regexes = regexes.slice(0, -3);
|
poolStats.pool.regexes = regexes.slice(0, -3);
|
||||||
|
poolStats.pool.addresses = poolStats.pool.addresses;
|
||||||
|
|
||||||
|
if (poolStats.reportedHashrate) {
|
||||||
|
poolStats.luck = poolStats.estimatedHashrate / poolStats.reportedHashrate * 100;
|
||||||
|
}
|
||||||
|
|
||||||
this.openGraphService.waitOver('pool-stats-' + this.slug);
|
this.openGraphService.waitOver('pool-stats-' + this.slug);
|
||||||
|
|
||||||
|
|||||||
@@ -38,12 +38,12 @@
|
|||||||
<tr *ngIf="!isMobile()" class="taller-row">
|
<tr *ngIf="!isMobile()" class="taller-row">
|
||||||
<td class="label addresses" i18n="mining.addresses">Addresses</td>
|
<td class="label addresses" i18n="mining.addresses">Addresses</td>
|
||||||
<td *ngIf="poolStats.pool.addresses.length else nodata" style="padding-top: 25px">
|
<td *ngIf="poolStats.pool.addresses.length else nodata" style="padding-top: 25px">
|
||||||
<a class="addresses-data" [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]">
|
<a [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]" class="first-address">
|
||||||
{{ poolStats.pool.addresses[0] }}
|
{{ poolStats.pool.addresses[0] }}
|
||||||
</a>
|
</a>
|
||||||
<div>
|
<div>
|
||||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg">
|
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg">
|
||||||
<a class="addresses-data" *ngFor="let address of poolStats.pool.addresses | slice: 1"
|
<a *ngFor="let address of poolStats.pool.addresses | slice: 1"
|
||||||
[routerLink]="['/address' | relativeUrl, address]">{{
|
[routerLink]="['/address' | relativeUrl, address]">{{
|
||||||
address }}<br></a>
|
address }}<br></a>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,13 +67,13 @@
|
|||||||
[attr.aria-expanded]="!gfg" aria-controls="collapseExample">
|
[attr.aria-expanded]="!gfg" aria-controls="collapseExample">
|
||||||
<span i18n="show-all">Show all</span> ({{ poolStats.pool.addresses.length }})
|
<span i18n="show-all">Show all</span> ({{ poolStats.pool.addresses.length }})
|
||||||
</button>
|
</button>
|
||||||
<a class="addresses-data" [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]">
|
<a [routerLink]="['/address' | relativeUrl, poolStats.pool.addresses[0]]">
|
||||||
{{ poolStats.pool.addresses[0] | shortenString: 30 }}
|
{{ poolStats.pool.addresses[0] | shortenString: 40 }}
|
||||||
</a>
|
</a>
|
||||||
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg" style="width: 100%">
|
<div #collapse="ngbCollapse" [(ngbCollapse)]="gfg" style="width: 100%">
|
||||||
<a class="addresses-data" *ngFor="let address of poolStats.pool.addresses | slice: 1"
|
<a *ngFor="let address of poolStats.pool.addresses | slice: 1"
|
||||||
[routerLink]="['/address' | relativeUrl, address]">{{
|
[routerLink]="['/address' | relativeUrl, address]">{{
|
||||||
address | shortenString: 30 }}<br></a>
|
address | shortenString: 40 }}<br></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -88,25 +88,22 @@
|
|||||||
|
|
||||||
<!-- Hashrate desktop -->
|
<!-- Hashrate desktop -->
|
||||||
<tr *ngIf="!isMobile()" class="taller-row">
|
<tr *ngIf="!isMobile()" class="taller-row">
|
||||||
|
<td class="label" i18n="mining.hashrate-24h">Hashrate (24h)</td>
|
||||||
<td class="data">
|
<td class="data">
|
||||||
<table class="table table-xs table-data">
|
<table class="table table-xs table-data">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.reward">Reward</th>
|
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.hashrate">Hashrate (24h)</th>
|
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="latest-blocks.avg_health">Avg Health</th>
|
<th scope="col" class="block-count-title" style="width: 26%" i18n="mining.luck">Luck</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<td class="text-center"><app-amount [satoshis]="poolStats.totalReward" digitsInfo="1.0-0" [noFiat]="true"></app-amount></td>
|
<td>{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td>
|
||||||
<td class="text-center">{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td>
|
<ng-template *ngIf="poolStats.luck; else noreported">
|
||||||
<td class="text-center"><span class="health-badge badge" [class.badge-success]="poolStats.avgBlockHealth >= 99"
|
<td>{{ poolStats.reportedHashrate | amountShortener : 1 : 'H/s' }}</td>
|
||||||
[class.badge-warning]="poolStats.avgBlockHealth >= 75 && poolStats.avgBlockHealth < 99" [class.badge-danger]="poolStats.avgBlockHealth < 75"
|
<td>{{ formatNumber(poolStats.luck, this.locale, '1.2-2') }}%</td>
|
||||||
*ngIf="poolStats.avgBlockHealth != null; else nullHealth">{{ poolStats.avgBlockHealth }}%</span>
|
</ng-template>
|
||||||
<ng-template #nullHealth>
|
|
||||||
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
|
|
||||||
</ng-template>
|
|
||||||
</td>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
@@ -114,46 +111,49 @@
|
|||||||
<!-- Hashrate mobile -->
|
<!-- Hashrate mobile -->
|
||||||
<tr *ngIf="isMobile()">
|
<tr *ngIf="isMobile()">
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
|
<span class="label" i18n="mining.hashrate-24h">Hashrate (24h)</span>
|
||||||
<table class="table table-xs table-data">
|
<table class="table table-xs table-data">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.reward">Reward</th>
|
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.hashrate">Hashrate (24h)</th>
|
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="latest-blocks.avg_health">Avg Health</th>
|
<th scope="col" class="block-count-title" style="width: 30%" i18n="mining.luck">Luck</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<td class="text-center"><app-amount [satoshis]="poolStats.totalReward" digitsInfo="1.0-0" [noFiat]="true"></app-amount></td>
|
<td>{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td>
|
||||||
<td class="text-center">{{ poolStats.estimatedHashrate | amountShortener : 1 : 'H/s' }}</td>
|
<ng-template *ngIf="poolStats.luck; else noreported">
|
||||||
<td class="text-center"><span class="health-badge badge" [class.badge-success]="poolStats.avgBlockHealth >= 99"
|
<td>{{ poolStats.reportedHashrate | amountShortener : 1 : 'H/s' }}</td>
|
||||||
[class.badge-warning]="poolStats.avgBlockHealth >= 75 && poolStats.avgBlockHealth < 99" [class.badge-danger]="poolStats.avgBlockHealth < 75"
|
<td>{{ formatNumber(poolStats.luck, this.locale, '1.2-2') }}%</td>
|
||||||
*ngIf="poolStats.avgBlockHealth != null; else nullHealth">{{ poolStats.avgBlockHealth }}%</span>
|
</ng-template>
|
||||||
<ng-template #nullHealth>
|
|
||||||
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
|
|
||||||
</ng-template>
|
|
||||||
</td>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<ng-template #noreported>
|
||||||
|
<td>~</td>
|
||||||
|
<td>~</td>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<!-- Mined blocks desktop -->
|
<!-- Mined blocks desktop -->
|
||||||
<tr *ngIf="!isMobile()" class="taller-row">
|
<tr *ngIf="!isMobile()" class="taller-row">
|
||||||
|
<td class="label" i18n="mining.mined-blocks">Mined blocks</td>
|
||||||
<td class="data">
|
<td class="data">
|
||||||
<table class="table table-xs table-data">
|
<table class="table table-xs table-data">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
<th scope="col" class="block-count-title" style="width: 37%" i18n="24h">24h</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
|
<th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
|
<th scope="col" class="block-count-title" style="width: 26%" i18n="all">All</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<td class="text-center">{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
<td>{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||||
poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td>
|
poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td>
|
||||||
<td class="text-center">{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
<td>{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||||
poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td>
|
poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td>
|
||||||
<td class="text-center">{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
<td>{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||||
poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td>
|
poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -162,20 +162,21 @@
|
|||||||
<!-- Mined blocks mobile -->
|
<!-- Mined blocks mobile -->
|
||||||
<tr *ngIf="isMobile()">
|
<tr *ngIf="isMobile()">
|
||||||
<td colspan=2>
|
<td colspan=2>
|
||||||
|
<span class="label" i18n="mining.mined-blocks">Mined blocks</span>
|
||||||
<table class="table table-xs table-data">
|
<table class="table table-xs table-data">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
<th scope="col" class="block-count-title" style="width: 33%" i18n="24h">24h</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
|
<th scope="col" class="block-count-title" style="width: 37%" i18n="1w">1w</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
|
<th scope="col" class="block-count-title" style="width: 30%" i18n="all">All</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<td class="text-center">{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
<td>{{ formatNumber(poolStats.blockCount['24h'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||||
poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td>
|
poolStats.blockShare['24h'], this.locale, '1.0-0') }}%)</td>
|
||||||
<td class="text-center">{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
<td>{{ formatNumber(poolStats.blockCount['1w'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||||
poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td>
|
poolStats.blockShare['1w'], this.locale, '1.0-0') }}%)</td>
|
||||||
<td class="text-center">{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
<td>{{ formatNumber(poolStats.blockCount['all'], this.locale, '1.0-0') }} ({{ formatNumber(100 *
|
||||||
poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td>
|
poolStats.blockShare['all'], this.locale, '1.0-0') }}%)</td>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -212,9 +213,8 @@
|
|||||||
<th class="timestamp" i18n="latest-blocks.timestamp">Timestamp</th>
|
<th class="timestamp" i18n="latest-blocks.timestamp">Timestamp</th>
|
||||||
<th class="mined" i18n="latest-blocks.mined">Mined</th>
|
<th class="mined" i18n="latest-blocks.mined">Mined</th>
|
||||||
<th class="coinbase text-left" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
|
<th class="coinbase text-left" i18n="latest-blocks.coinbasetag">Coinbase tag</th>
|
||||||
<th *ngIf="auditAvailable" class="health text-right" i18n="latest-blocks.health">Health</th>
|
|
||||||
<th class="reward text-right" i18n="latest-blocks.reward">Reward</th>
|
<th class="reward text-right" i18n="latest-blocks.reward">Reward</th>
|
||||||
<th *ngIf="!auditAvailable" class="fees text-right" i18n="latest-blocks.fees">Fees</th>
|
<th class="fees text-right" i18n="latest-blocks.fees">Fees</th>
|
||||||
<th class="txs text-right" i18n="dashboard.txs">TXs</th>
|
<th class="txs text-right" i18n="dashboard.txs">TXs</th>
|
||||||
<th class="size" i18n="latest-blocks.size">Size</th>
|
<th class="size" i18n="latest-blocks.size">Size</th>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -227,31 +227,17 @@
|
|||||||
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
‎{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||||
</td>
|
</td>
|
||||||
<td class="mined">
|
<td class="mined">
|
||||||
<app-time kind="since" [time]="block.timestamp" [fastRender]="true"></app-time>
|
<app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since>
|
||||||
</td>
|
</td>
|
||||||
<td class="coinbase">
|
<td class="coinbase">
|
||||||
<span class="badge badge-secondary scriptmessage longer">
|
<span class="badge badge-secondary scriptmessage longer">
|
||||||
{{ block.extras.coinbaseRaw | hex2ascii }}
|
{{ block.extras.coinbaseRaw | hex2ascii }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="auditAvailable" class="health text-right">
|
|
||||||
<a
|
|
||||||
class="health-badge badge"
|
|
||||||
[class.badge-success]="block.extras.matchRate >= 99"
|
|
||||||
[class.badge-warning]="block.extras.matchRate >= 75 && block.extras.matchRate < 99"
|
|
||||||
[class.badge-danger]="block.extras.matchRate < 75"
|
|
||||||
[routerLink]="block.extras.matchRate != null ? ['/block/' | relativeUrl, block.id] : null"
|
|
||||||
[state]="{ data: { block: block } }"
|
|
||||||
*ngIf="block.extras.matchRate != null; else nullHealth"
|
|
||||||
>{{ block.extras.matchRate }}%</a>
|
|
||||||
<ng-template #nullHealth>
|
|
||||||
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
|
|
||||||
</ng-template>
|
|
||||||
</td>
|
|
||||||
<td class="reward text-right">
|
<td class="reward text-right">
|
||||||
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
|
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="!auditAvailable" class="fees text-right">
|
<td class="fees text-right">
|
||||||
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
|
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-2" [noFiat]="true"></app-amount>
|
||||||
</td>
|
</td>
|
||||||
<td class="txs text-right">
|
<td class="txs text-right">
|
||||||
@@ -378,23 +364,24 @@
|
|||||||
|
|
||||||
<!-- Hashrate desktop -->
|
<!-- Hashrate desktop -->
|
||||||
<tr *ngIf="!isMobile()" class="taller-row">
|
<tr *ngIf="!isMobile()" class="taller-row">
|
||||||
|
<td class="label" i18n="mining.hashrate-24h">Hashrate (24h)</td>
|
||||||
<td class="data">
|
<td class="data">
|
||||||
<table class="table table-xs table-data text-center">
|
<table class="table table-xs table-data text-center">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.total-reward">Reward</th>
|
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.estimated">Estimated</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th>
|
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.luck">Avg Health</th>
|
<th scope="col" class="block-count-title" style="width: 26%" i18n="mining.luck">Luck</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<div class="skeleton-loader data"></div>
|
<div class="skeleton-loader data"></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<div class="skeleton-loader data"></div>
|
<div class="skeleton-loader data"></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<div class="skeleton-loader data"></div>
|
<div class="skeleton-loader data"></div>
|
||||||
</td>
|
</td>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -404,22 +391,23 @@
|
|||||||
<!-- Hashrate mobile -->
|
<!-- Hashrate mobile -->
|
||||||
<tr *ngIf="isMobile()">
|
<tr *ngIf="isMobile()">
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
|
<span class="label" i18n="mining.hashrate-24h">Hashrate (24h)</span>
|
||||||
<table class="table table-xs table-data text-center">
|
<table class="table table-xs table-data text-center">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.total-reward">Reward</th>
|
<th scope="col" class="block-count-title" style="width: 33%" i18n="mining.estimated">Estimated</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.estimated">Hashrate (24h)</th>
|
<th scope="col" class="block-count-title" style="width: 37%" i18n="mining.reported">Reported</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="mining.luck">Avg Health</th>
|
<th scope="col" class="block-count-title" style="width: 30%" i18n="mining.luck">Luck</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<div class="skeleton-loader data"></div>
|
<div class="skeleton-loader data"></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<div class="skeleton-loader data"></div>
|
<div class="skeleton-loader data"></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<div class="skeleton-loader data"></div>
|
<div class="skeleton-loader data"></div>
|
||||||
</td>
|
</td>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -429,23 +417,24 @@
|
|||||||
|
|
||||||
<!-- Mined blocks desktop -->
|
<!-- Mined blocks desktop -->
|
||||||
<tr *ngIf="!isMobile()" class="taller-row">
|
<tr *ngIf="!isMobile()" class="taller-row">
|
||||||
|
<td class="label" i18n="mining.mined-blocks">Mined blocks</td>
|
||||||
<td class="data">
|
<td class="data">
|
||||||
<table class="table table-xs table-data text-center">
|
<table class="table table-xs table-data text-center">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
<th scope="col" class="block-count-title" style="width: 37%">24h</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
|
<th scope="col" class="block-count-title" style="width: 37%">1w</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
|
<th scope="col" class="block-count-title" style="width: 26%" i18n="all">All</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<div class="skeleton-loader data"></div>
|
<div class="skeleton-loader data"></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<div class="skeleton-loader data"></div>
|
<div class="skeleton-loader data"></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<div class="skeleton-loader data"></div>
|
<div class="skeleton-loader data"></div>
|
||||||
</td>
|
</td>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -455,22 +444,23 @@
|
|||||||
<!-- Mined blocks mobile -->
|
<!-- Mined blocks mobile -->
|
||||||
<tr *ngIf="isMobile()">
|
<tr *ngIf="isMobile()">
|
||||||
<td colspan=2>
|
<td colspan=2>
|
||||||
|
<span class="label" i18n="mining.mined-blocks">Mined blocks</span>
|
||||||
<table class="table table-xs table-data text-center">
|
<table class="table table-xs table-data text-center">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="24h">Blocks 24h</th>
|
<th scope="col" class="block-count-title" style="width: 33%">24h</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="1w">1w</th>
|
<th scope="col" class="block-count-title" style="width: 37%">1w</th>
|
||||||
<th scope="col" class="block-count-title text-center" style="width: 33%" i18n="all">All</th>
|
<th scope="col" class="block-count-title" style="width: 30%" i18n="all">All</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<div class="skeleton-loader data"></div>
|
<div class="skeleton-loader data"></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<div class="skeleton-loader data"></div>
|
<div class="skeleton-loader data"></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td>
|
||||||
<div class="skeleton-loader data"></div>
|
<div class="skeleton-loader data"></div>
|
||||||
</td>
|
</td>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -68,11 +68,6 @@ div.scrollable {
|
|||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
padding-top: 25px;
|
padding-top: 25px;
|
||||||
}
|
}
|
||||||
.addresses-data {
|
|
||||||
vertical-align: top;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data {
|
.data {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@@ -105,7 +100,7 @@ div.scrollable {
|
|||||||
@media (max-width: 875px) {
|
@media (max-width: 875px) {
|
||||||
padding-left: 50px;
|
padding-left: 50px;
|
||||||
}
|
}
|
||||||
@media (max-width: 685px) {
|
@media (max-width: 650px) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,7 +118,7 @@ div.scrollable {
|
|||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
@media (max-width: 875px) {
|
@media (max-width: 875px) {
|
||||||
padding-right: 20px;
|
padding-right: 50px;
|
||||||
}
|
}
|
||||||
@media (max-width: 567px) {
|
@media (max-width: 567px) {
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
@@ -191,6 +186,10 @@ div.scrollable {
|
|||||||
.block-count-title {
|
.block-count-title {
|
||||||
color: #4a68b9;
|
color: #4a68b9;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
text-align: left;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-data tr {
|
.table-data tr {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { EChartsOption, graphic } from 'echarts';
|
import { EChartsOption, graphic } from 'echarts';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable, timer } from 'rxjs';
|
||||||
import { distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
|
import { distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
|
||||||
import { BlockExtended, PoolStat } from '../../interfaces/node-api.interface';
|
import { BlockExtended, PoolStat } from '../../interfaces/node-api.interface';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
@@ -35,8 +35,6 @@ export class PoolComponent implements OnInit {
|
|||||||
blocks: BlockExtended[] = [];
|
blocks: BlockExtended[] = [];
|
||||||
slug: string = undefined;
|
slug: string = undefined;
|
||||||
|
|
||||||
auditAvailable = false;
|
|
||||||
|
|
||||||
loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[this.blocks.length - 1]?.height);
|
loadMoreSubject: BehaviorSubject<number> = new BehaviorSubject(this.blocks[this.blocks.length - 1]?.height);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -46,7 +44,6 @@ export class PoolComponent implements OnInit {
|
|||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
) {
|
) {
|
||||||
this.auditAvailable = this.stateService.env.AUDIT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -77,6 +74,11 @@ export class PoolComponent implements OnInit {
|
|||||||
regexes += regex + '", "';
|
regexes += regex + '", "';
|
||||||
}
|
}
|
||||||
poolStats.pool.regexes = regexes.slice(0, -3);
|
poolStats.pool.regexes = regexes.slice(0, -3);
|
||||||
|
poolStats.pool.addresses = poolStats.pool.addresses;
|
||||||
|
|
||||||
|
if (poolStats.reportedHashrate) {
|
||||||
|
poolStats.luck = poolStats.estimatedHashrate / poolStats.reportedHashrate * 100;
|
||||||
|
}
|
||||||
|
|
||||||
return Object.assign({
|
return Object.assign({
|
||||||
logo: `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'
|
logo: `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<div class="search-box-container mr-2">
|
<div class="search-box-container mr-2">
|
||||||
<input autofocus (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
<input (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
||||||
<app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
|
<app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -53,8 +53,3 @@ form {
|
|||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus {
|
|
||||||
box-shadow: none;
|
|
||||||
border-color: #1b1f2c;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -85,20 +85,21 @@ export class StartComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
this.stateService.blocks$
|
this.stateService.blocks$
|
||||||
.subscribe((blocks: any) => {
|
.subscribe((blocks: any) => {
|
||||||
|
if (this.stateService.network !== '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.countdown = 0;
|
this.countdown = 0;
|
||||||
const block = blocks[0];
|
const block = blocks[0];
|
||||||
|
|
||||||
for (const sb in specialBlocks) {
|
for (const sb in specialBlocks) {
|
||||||
if (specialBlocks[sb].networks.includes(this.stateService.network || 'mainnet')) {
|
const height = parseInt(sb, 10);
|
||||||
const height = parseInt(sb, 10);
|
const diff = height - block.height;
|
||||||
const diff = height - block.height;
|
if (diff > 0 && diff <= 1008) {
|
||||||
if (diff > 0 && diff <= 1008) {
|
this.countdown = diff;
|
||||||
this.countdown = diff;
|
this.eventName = specialBlocks[sb].labelEvent;
|
||||||
this.eventName = specialBlocks[sb].labelEvent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (specialBlocks[block.height] && specialBlocks[block.height].networks.includes(this.stateService.network || 'mainnet')) {
|
if (specialBlocks[block.height]) {
|
||||||
this.specialEvent = true;
|
this.specialEvent = true;
|
||||||
this.eventName = specialBlocks[block.height].labelEventCompleted;
|
this.eventName = specialBlocks[block.height].labelEventCompleted;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -49,9 +49,6 @@
|
|||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '3y'">
|
||||||
<input type="radio" [value]="'3y'" [routerLink]="['/graphs' | relativeUrl]" fragment="3y" formControlName="dateSpan"> 3Y
|
<input type="radio" [value]="'3y'" [routerLink]="['/graphs' | relativeUrl]" fragment="3y" formControlName="dateSpan"> 3Y
|
||||||
</label>
|
</label>
|
||||||
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '4y'">
|
|
||||||
<input type="radio" [value]="'4y'" [routerLink]="['/graphs' | relativeUrl]" fragment="4y" formControlName="dateSpan"> 4Y
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="small-buttons">
|
<div class="small-buttons">
|
||||||
<div ngbDropdown #myDrop="ngbDropdown">
|
<div ngbDropdown #myDrop="ngbDropdown">
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export class StatisticsComponent implements OnInit {
|
|||||||
this.route
|
this.route
|
||||||
.fragment
|
.fragment
|
||||||
.subscribe((fragment) => {
|
.subscribe((fragment) => {
|
||||||
if (['2h', '24h', '1w', '1m', '3m', '6m', '1y', '2y', '3y', '4y'].indexOf(fragment) > -1) {
|
if (['2h', '24h', '1w', '1m', '3m', '6m', '1y', '2y', '3y'].indexOf(fragment) > -1) {
|
||||||
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
|
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -109,10 +109,7 @@ export class StatisticsComponent implements OnInit {
|
|||||||
if (this.radioGroupForm.controls.dateSpan.value === '2y') {
|
if (this.radioGroupForm.controls.dateSpan.value === '2y') {
|
||||||
return this.apiService.list2YStatistics$();
|
return this.apiService.list2YStatistics$();
|
||||||
}
|
}
|
||||||
if (this.radioGroupForm.controls.dateSpan.value === '3y') {
|
return this.apiService.list3YStatistics$();
|
||||||
return this.apiService.list3YStatistics$();
|
|
||||||
}
|
|
||||||
return this.apiService.list4YStatistics$();
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.subscribe((mempoolStats: any) => {
|
.subscribe((mempoolStats: any) => {
|
||||||
@@ -184,7 +181,7 @@ export class StatisticsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let capRatio = 10;
|
let capRatio = 10;
|
||||||
if (['1m', '3m', '6m', '1y', '2y', '3y', '4y'].includes(this.graphWindowPreference)) {
|
if (['1m', '3m', '6m', '1y', '2y', '3y'].includes(this.graphWindowPreference)) {
|
||||||
capRatio = 4;
|
capRatio = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,12 +25,6 @@
|
|||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngSwitchCase="'warning'">
|
|
||||||
<svg [class]="class" [style]="style" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M135.3 34.474c-15.62 27.306-54.206 95.63-85.21 150.534L9.075 257.583C5.382 264.08 6.76 269.217 7.908 271.7c2.326 5.028 7.29 7.537 11.155 8.215l.78.133 264.698.006-.554-.02c4.152.255 9.664-1.24 12.677-6.194 1.926-3.18 3.31-8.589-1.073-16.278L213.637 114.37l-45.351-79.205c-5.681-9.932-12.272-12.022-16.8-12.022-4.42 0-10.818 1.964-16.181 11.331h-.006zm-69.072 159.94c30.997-54.885 69.563-123.184 85.16-150.446l.186-.297c.2.303.393.582.618.981l45.363 79.22s72.377 126.47 78.569 137.283l-247.618-.007 37.719-66.734" style="fill:#ffc107;fill-opacity:1"/>
|
|
||||||
<path d="M152.597 247.445c8.02 0 14.518-6.728 14.518-15.025 0-8.29-6.499-15.018-14.518-15.018-8.031 0-14.529 6.728-14.529 15.018 0 8.297 6.498 15.025 14.53 15.025m-.001-147.18c11.586 0 22.23 10.958 20.977 21.7l-9.922 75.564c-.966 6.601-4.95 11.433-11.055 11.433s-10.102-4.832-11.056-11.433l-9.927-75.564c-1.26-10.742 9.39-21.7 20.983-21.7" style="fill:#ffc107;fill-opacity:1"/>
|
|
||||||
</svg>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngSwitchCase="'mempoolSpace'">
|
<ng-container *ngSwitchCase="'mempoolSpace'">
|
||||||
<svg [class]="class" [style]="style" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg [class]="class" [style]="style" [attr.width]="width" [attr.height]="height" [attr.viewBox]="viewBox" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M 219.548 86.198 L 219.548 63.833 C 219.548 60.359 218.746 57.686 217.163 55.919 C 215.601 54.151 213.237 53.267 210.195 53.267 C 206.762 53.267 203.946 54.377 202.013 56.453 C 200.081 58.55 199.053 61.633 199.053 65.395 L 199.053 86.219 L 191.447 86.219 L 191.447 63.833 C 191.447 56.823 188.282 53.267 182.032 53.267 C 178.6 53.267 175.783 54.377 173.851 56.453 C 171.919 58.55 170.891 61.633 170.891 65.395 L 170.891 86.219 L 163.285 86.219 L 163.285 46.422 L 170.685 46.422 L 170.685 50.759 C 173.687 47.799 178.003 46.175 182.999 46.175 C 188.96 46.175 193.667 48.498 196.36 52.753 C 199.608 48.559 204.85 46.175 210.955 46.175 C 215.93 46.175 219.877 47.614 222.693 50.43 C 225.632 53.39 227.174 57.871 227.154 63.36 L 227.154 86.198 L 219.548 86.198 Z" fill="white"/>
|
<path d="M 219.548 86.198 L 219.548 63.833 C 219.548 60.359 218.746 57.686 217.163 55.919 C 215.601 54.151 213.237 53.267 210.195 53.267 C 206.762 53.267 203.946 54.377 202.013 56.453 C 200.081 58.55 199.053 61.633 199.053 65.395 L 199.053 86.219 L 191.447 86.219 L 191.447 63.833 C 191.447 56.823 188.282 53.267 182.032 53.267 C 178.6 53.267 175.783 54.377 173.851 56.453 C 171.919 58.55 170.891 61.633 170.891 65.395 L 170.891 86.219 L 163.285 86.219 L 163.285 46.422 L 170.685 46.422 L 170.685 50.759 C 173.687 47.799 178.003 46.175 182.999 46.175 C 188.96 46.175 193.667 48.498 196.36 52.753 C 199.608 48.559 204.85 46.175 210.955 46.175 C 215.93 46.175 219.877 47.614 222.693 50.43 C 225.632 53.39 227.174 57.871 227.154 63.36 L 227.154 86.198 L 219.548 86.198 Z" fill="white"/>
|
||||||
@@ -110,4 +104,4 @@
|
|||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -13,5 +13,4 @@ export class SvgImagesComponent {
|
|||||||
@Input() width: string;
|
@Input() width: string;
|
||||||
@Input() height: string;
|
@Input() height: string;
|
||||||
@Input() viewBox: string;
|
@Input() viewBox: string;
|
||||||
@Input() fill: string;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
|
||||||
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { dates } from '../../shared/i18n/dates';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-time-since',
|
||||||
|
template: `{{ text }}`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class TimeSinceComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
|
interval: number;
|
||||||
|
text: string;
|
||||||
|
intervals = {};
|
||||||
|
|
||||||
|
@Input() time: number;
|
||||||
|
@Input() dateString: number;
|
||||||
|
@Input() fastRender = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private ref: ChangeDetectorRef,
|
||||||
|
private stateService: StateService,
|
||||||
|
) {
|
||||||
|
this.intervals = {
|
||||||
|
year: 31536000,
|
||||||
|
month: 2592000,
|
||||||
|
week: 604800,
|
||||||
|
day: 86400,
|
||||||
|
hour: 3600,
|
||||||
|
minute: 60,
|
||||||
|
second: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (!this.stateService.isBrowser) {
|
||||||
|
this.text = this.calculate();
|
||||||
|
this.ref.markForCheck();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.interval = window.setInterval(() => {
|
||||||
|
this.text = this.calculate();
|
||||||
|
this.ref.markForCheck();
|
||||||
|
}, 1000 * (this.fastRender ? 1 : 60));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges() {
|
||||||
|
this.text = this.calculate();
|
||||||
|
this.ref.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate() {
|
||||||
|
let date: Date;
|
||||||
|
if (this.dateString) {
|
||||||
|
date = new Date(this.dateString)
|
||||||
|
} else {
|
||||||
|
date = new Date(this.time * 1000);
|
||||||
|
}
|
||||||
|
const seconds = Math.floor((+new Date() - +date) / 1000);
|
||||||
|
if (seconds < 60) {
|
||||||
|
return $localize`:@@date-base.just-now:Just now`;
|
||||||
|
}
|
||||||
|
let counter: number;
|
||||||
|
for (const i in this.intervals) {
|
||||||
|
if (this.intervals.hasOwnProperty(i)) {
|
||||||
|
counter = Math.floor(seconds / this.intervals[i]);
|
||||||
|
const dateStrings = dates(counter);
|
||||||
|
if (counter > 0) {
|
||||||
|
if (counter === 1) {
|
||||||
|
switch (i) { // singular (1 day)
|
||||||
|
case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break;
|
||||||
|
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break;
|
||||||
|
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break;
|
||||||
|
case 'day': return $localize`:@@time-since:${dateStrings.i18nDay}:DATE: ago`; break;
|
||||||
|
case 'hour': return $localize`:@@time-since:${dateStrings.i18nHour}:DATE: ago`; break;
|
||||||
|
case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinute}:DATE: ago`; break;
|
||||||
|
case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (i) { // plural (2 days)
|
||||||
|
case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break;
|
||||||
|
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break;
|
||||||
|
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break;
|
||||||
|
case 'day': return $localize`:@@time-since:${dateStrings.i18nDays}:DATE: ago`; break;
|
||||||
|
case 'hour': return $localize`:@@time-since:${dateStrings.i18nHours}:DATE: ago`; break;
|
||||||
|
case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinutes}:DATE: ago`; break;
|
||||||
|
case 'second': return $localize`:@@time-since:${dateStrings.i18nSeconds}:DATE: ago`; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
91
frontend/src/app/components/time-span/time-span.component.ts
Normal file
91
frontend/src/app/components/time-span/time-span.component.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
|
||||||
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { dates } from '../../shared/i18n/dates';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-time-span',
|
||||||
|
template: `{{ text }}`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class TimeSpanComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
|
interval: number;
|
||||||
|
text: string;
|
||||||
|
intervals = {};
|
||||||
|
|
||||||
|
@Input() time: number;
|
||||||
|
@Input() fastRender = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private ref: ChangeDetectorRef,
|
||||||
|
private stateService: StateService,
|
||||||
|
) {
|
||||||
|
this.intervals = {
|
||||||
|
year: 31536000,
|
||||||
|
month: 2592000,
|
||||||
|
week: 604800,
|
||||||
|
day: 86400,
|
||||||
|
hour: 3600,
|
||||||
|
minute: 60,
|
||||||
|
second: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (!this.stateService.isBrowser) {
|
||||||
|
this.text = this.calculate();
|
||||||
|
this.ref.markForCheck();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.interval = window.setInterval(() => {
|
||||||
|
this.text = this.calculate();
|
||||||
|
this.ref.markForCheck();
|
||||||
|
}, 1000 * (this.fastRender ? 1 : 60));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges() {
|
||||||
|
this.text = this.calculate();
|
||||||
|
this.ref.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate() {
|
||||||
|
const seconds = Math.floor(this.time);
|
||||||
|
if (seconds < 60) {
|
||||||
|
return $localize`:@@date-base.just-now:Just now`;
|
||||||
|
}
|
||||||
|
let counter: number;
|
||||||
|
for (const i in this.intervals) {
|
||||||
|
if (this.intervals.hasOwnProperty(i)) {
|
||||||
|
counter = Math.floor(seconds / this.intervals[i]);
|
||||||
|
const dateStrings = dates(counter);
|
||||||
|
if (counter > 0) {
|
||||||
|
if (counter === 1) {
|
||||||
|
switch (i) { // singular (1 day)
|
||||||
|
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break;
|
||||||
|
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break;
|
||||||
|
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break;
|
||||||
|
case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDay}:DATE:`; break;
|
||||||
|
case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHour}:DATE:`; break;
|
||||||
|
case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinute}:DATE:`; break;
|
||||||
|
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (i) { // plural (2 days)
|
||||||
|
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break;
|
||||||
|
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break;
|
||||||
|
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break;
|
||||||
|
case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDays}:DATE:`; break;
|
||||||
|
case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHours}:DATE:`; break;
|
||||||
|
case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinutes}:DATE:`; break;
|
||||||
|
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSeconds}:DATE:`; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
104
frontend/src/app/components/time-until/time-until.component.ts
Normal file
104
frontend/src/app/components/time-until/time-until.component.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
|
||||||
|
import { StateService } from '../../services/state.service';
|
||||||
|
import { dates } from '../../shared/i18n/dates';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-time-until',
|
||||||
|
template: `{{ text }}`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class TimeUntilComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
|
interval: number;
|
||||||
|
text: string;
|
||||||
|
intervals = {};
|
||||||
|
|
||||||
|
@Input() time: number;
|
||||||
|
@Input() fastRender = false;
|
||||||
|
@Input() fixedRender = false;
|
||||||
|
@Input() forceFloorOnTimeIntervals: string[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private ref: ChangeDetectorRef,
|
||||||
|
private stateService: StateService,
|
||||||
|
) {
|
||||||
|
this.intervals = {
|
||||||
|
year: 31536000,
|
||||||
|
month: 2592000,
|
||||||
|
week: 604800,
|
||||||
|
day: 86400,
|
||||||
|
hour: 3600,
|
||||||
|
minute: 60,
|
||||||
|
second: 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if(this.fixedRender){
|
||||||
|
this.text = this.calculate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.stateService.isBrowser) {
|
||||||
|
this.text = this.calculate();
|
||||||
|
this.ref.markForCheck();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.interval = window.setInterval(() => {
|
||||||
|
this.text = this.calculate();
|
||||||
|
this.ref.markForCheck();
|
||||||
|
}, 1000 * (this.fastRender ? 1 : 60));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges() {
|
||||||
|
this.text = this.calculate();
|
||||||
|
this.ref.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculate() {
|
||||||
|
const seconds = (+new Date(this.time) - +new Date()) / 1000;
|
||||||
|
|
||||||
|
if (seconds < 60) {
|
||||||
|
const dateStrings = dates(1);
|
||||||
|
return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`;
|
||||||
|
}
|
||||||
|
let counter: number;
|
||||||
|
for (const i in this.intervals) {
|
||||||
|
if (this.intervals.hasOwnProperty(i)) {
|
||||||
|
if (this.forceFloorOnTimeIntervals && this.forceFloorOnTimeIntervals.indexOf(i) > -1) {
|
||||||
|
counter = Math.floor(seconds / this.intervals[i]);
|
||||||
|
} else {
|
||||||
|
counter = Math.round(seconds / this.intervals[i]);
|
||||||
|
}
|
||||||
|
const dateStrings = dates(counter);
|
||||||
|
if (counter > 0) {
|
||||||
|
if (counter === 1) {
|
||||||
|
switch (i) { // singular (In ~1 day)
|
||||||
|
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break;
|
||||||
|
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break;
|
||||||
|
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break;
|
||||||
|
case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDay}:DATE:`; break;
|
||||||
|
case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHour}:DATE:`; break;
|
||||||
|
case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`;
|
||||||
|
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (i) { // plural (In ~2 days)
|
||||||
|
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break;
|
||||||
|
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break;
|
||||||
|
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break;
|
||||||
|
case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDays}:DATE:`; break;
|
||||||
|
case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHours}:DATE:`; break;
|
||||||
|
case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinutes}:DATE:`; break;
|
||||||
|
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSeconds}:DATE:`; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnChanges } from '@angular/core';
|
|
||||||
import { StateService } from '../../services/state.service';
|
|
||||||
import { dates } from '../../shared/i18n/dates';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-time',
|
|
||||||
template: `{{ text }}`,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
|
||||||
})
|
|
||||||
export class TimeComponent implements OnInit, OnChanges, OnDestroy {
|
|
||||||
interval: number;
|
|
||||||
text: string;
|
|
||||||
intervals = {};
|
|
||||||
|
|
||||||
@Input() time: number;
|
|
||||||
@Input() dateString: number;
|
|
||||||
@Input() kind: 'plain' | 'since' | 'until' | 'span' = 'plain';
|
|
||||||
@Input() fastRender = false;
|
|
||||||
@Input() fixedRender = false;
|
|
||||||
@Input() relative = false;
|
|
||||||
@Input() forceFloorOnTimeIntervals: string[];
|
|
||||||
@Input() fractionDigits: number = 0;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private ref: ChangeDetectorRef,
|
|
||||||
private stateService: StateService,
|
|
||||||
) {
|
|
||||||
this.intervals = {
|
|
||||||
year: 31536000,
|
|
||||||
month: 2592000,
|
|
||||||
week: 604800,
|
|
||||||
day: 86400,
|
|
||||||
hour: 3600,
|
|
||||||
minute: 60,
|
|
||||||
second: 1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
if(this.fixedRender){
|
|
||||||
this.text = this.calculate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.stateService.isBrowser) {
|
|
||||||
this.text = this.calculate();
|
|
||||||
this.ref.markForCheck();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.interval = window.setInterval(() => {
|
|
||||||
this.text = this.calculate();
|
|
||||||
this.ref.markForCheck();
|
|
||||||
}, 1000 * (this.fastRender ? 1 : 60));
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges() {
|
|
||||||
this.text = this.calculate();
|
|
||||||
this.ref.markForCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
clearInterval(this.interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
calculate() {
|
|
||||||
let seconds: number;
|
|
||||||
switch (this.kind) {
|
|
||||||
case 'since':
|
|
||||||
seconds = Math.floor((+new Date() - +new Date(this.dateString || this.time * 1000)) / 1000);
|
|
||||||
break;
|
|
||||||
case 'until':
|
|
||||||
seconds = (+new Date(this.time) - +new Date()) / 1000;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
seconds = Math.floor(this.time);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seconds < 60) {
|
|
||||||
if (this.relative || this.kind === 'since') {
|
|
||||||
return $localize`:@@date-base.just-now:Just now`;
|
|
||||||
} else if (this.kind === 'until') {
|
|
||||||
seconds = 60;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let counter: number;
|
|
||||||
for (const i in this.intervals) {
|
|
||||||
if (this.kind !== 'until' || this.forceFloorOnTimeIntervals && this.forceFloorOnTimeIntervals.indexOf(i) > -1) {
|
|
||||||
counter = Math.floor(seconds / this.intervals[i]);
|
|
||||||
} else {
|
|
||||||
counter = Math.round(seconds / this.intervals[i]);
|
|
||||||
}
|
|
||||||
let rounded = counter;
|
|
||||||
if (this.fractionDigits) {
|
|
||||||
const roundFactor = Math.pow(10,this.fractionDigits);
|
|
||||||
rounded = Math.round((seconds / this.intervals[i]) * roundFactor) / roundFactor;
|
|
||||||
}
|
|
||||||
const dateStrings = dates(rounded);
|
|
||||||
if (counter > 0) {
|
|
||||||
switch (this.kind) {
|
|
||||||
case 'since':
|
|
||||||
if (counter === 1) {
|
|
||||||
switch (i) { // singular (1 day)
|
|
||||||
case 'year': return $localize`:@@time-since:${dateStrings.i18nYear}:DATE: ago`; break;
|
|
||||||
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonth}:DATE: ago`; break;
|
|
||||||
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeek}:DATE: ago`; break;
|
|
||||||
case 'day': return $localize`:@@time-since:${dateStrings.i18nDay}:DATE: ago`; break;
|
|
||||||
case 'hour': return $localize`:@@time-since:${dateStrings.i18nHour}:DATE: ago`; break;
|
|
||||||
case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinute}:DATE: ago`; break;
|
|
||||||
case 'second': return $localize`:@@time-since:${dateStrings.i18nSecond}:DATE: ago`; break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (i) { // plural (2 days)
|
|
||||||
case 'year': return $localize`:@@time-since:${dateStrings.i18nYears}:DATE: ago`; break;
|
|
||||||
case 'month': return $localize`:@@time-since:${dateStrings.i18nMonths}:DATE: ago`; break;
|
|
||||||
case 'week': return $localize`:@@time-since:${dateStrings.i18nWeeks}:DATE: ago`; break;
|
|
||||||
case 'day': return $localize`:@@time-since:${dateStrings.i18nDays}:DATE: ago`; break;
|
|
||||||
case 'hour': return $localize`:@@time-since:${dateStrings.i18nHours}:DATE: ago`; break;
|
|
||||||
case 'minute': return $localize`:@@time-since:${dateStrings.i18nMinutes}:DATE: ago`; break;
|
|
||||||
case 'second': return $localize`:@@time-since:${dateStrings.i18nSeconds}:DATE: ago`; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'until':
|
|
||||||
if (counter === 1) {
|
|
||||||
switch (i) { // singular (In ~1 day)
|
|
||||||
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYear}:DATE:`; break;
|
|
||||||
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonth}:DATE:`; break;
|
|
||||||
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeek}:DATE:`; break;
|
|
||||||
case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDay}:DATE:`; break;
|
|
||||||
case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHour}:DATE:`; break;
|
|
||||||
case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinute}:DATE:`;
|
|
||||||
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSecond}:DATE:`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (i) { // plural (In ~2 days)
|
|
||||||
case 'year': return $localize`:@@time-until:In ~${dateStrings.i18nYears}:DATE:`; break;
|
|
||||||
case 'month': return $localize`:@@time-until:In ~${dateStrings.i18nMonths}:DATE:`; break;
|
|
||||||
case 'week': return $localize`:@@time-until:In ~${dateStrings.i18nWeeks}:DATE:`; break;
|
|
||||||
case 'day': return $localize`:@@time-until:In ~${dateStrings.i18nDays}:DATE:`; break;
|
|
||||||
case 'hour': return $localize`:@@time-until:In ~${dateStrings.i18nHours}:DATE:`; break;
|
|
||||||
case 'minute': return $localize`:@@time-until:In ~${dateStrings.i18nMinutes}:DATE:`; break;
|
|
||||||
case 'second': return $localize`:@@time-until:In ~${dateStrings.i18nSeconds}:DATE:`; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'span':
|
|
||||||
if (counter === 1) {
|
|
||||||
switch (i) { // singular (1 day)
|
|
||||||
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYear}:DATE:`; break;
|
|
||||||
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonth}:DATE:`; break;
|
|
||||||
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeek}:DATE:`; break;
|
|
||||||
case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDay}:DATE:`; break;
|
|
||||||
case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHour}:DATE:`; break;
|
|
||||||
case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinute}:DATE:`; break;
|
|
||||||
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSecond}:DATE:`; break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (i) { // plural (2 days)
|
|
||||||
case 'year': return $localize`:@@time-span:After ${dateStrings.i18nYears}:DATE:`; break;
|
|
||||||
case 'month': return $localize`:@@time-span:After ${dateStrings.i18nMonths}:DATE:`; break;
|
|
||||||
case 'week': return $localize`:@@time-span:After ${dateStrings.i18nWeeks}:DATE:`; break;
|
|
||||||
case 'day': return $localize`:@@time-span:After ${dateStrings.i18nDays}:DATE:`; break;
|
|
||||||
case 'hour': return $localize`:@@time-span:After ${dateStrings.i18nHours}:DATE:`; break;
|
|
||||||
case 'minute': return $localize`:@@time-span:After ${dateStrings.i18nMinutes}:DATE:`; break;
|
|
||||||
case 'second': return $localize`:@@time-span:After ${dateStrings.i18nSeconds}:DATE:`; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (counter === 1) {
|
|
||||||
switch (i) { // singular (1 day)
|
|
||||||
case 'year': return dateStrings.i18nYear; break;
|
|
||||||
case 'month': return dateStrings.i18nMonth; break;
|
|
||||||
case 'week': return dateStrings.i18nWeek; break;
|
|
||||||
case 'day': return dateStrings.i18nDay; break;
|
|
||||||
case 'hour': return dateStrings.i18nHour; break;
|
|
||||||
case 'minute': return dateStrings.i18nMinute; break;
|
|
||||||
case 'second': return dateStrings.i18nSecond; break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch (i) { // plural (2 days)
|
|
||||||
case 'year': return dateStrings.i18nYears; break;
|
|
||||||
case 'month': return dateStrings.i18nMonths; break;
|
|
||||||
case 'week': return dateStrings.i18nWeeks; break;
|
|
||||||
case 'day': return dateStrings.i18nDays; break;
|
|
||||||
case 'hour': return dateStrings.i18nHours; break;
|
|
||||||
case 'minute': return dateStrings.i18nMinutes; break;
|
|
||||||
case 'second': return dateStrings.i18nSeconds; break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -57,17 +57,17 @@
|
|||||||
<td>
|
<td>
|
||||||
‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
‎{{ tx.status.block_time * 1000 | date:'yyyy-MM-dd HH:mm' }}
|
||||||
<div class="lg-inline">
|
<div class="lg-inline">
|
||||||
<i class="symbol">(<app-time kind="since" [time]="tx.status.block_time" [fastRender]="true"></app-time>)</i>
|
<i class="symbol">(<app-time-since [time]="tx.status.block_time" [fastRender]="true"></app-time-since>)</i>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<ng-template [ngIf]="transactionTime > 0">
|
<ng-template [ngIf]="transactionTime > 0">
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="transaction.confirmed|Transaction Confirmed state">Confirmed</td>
|
<td i18n="transaction.confirmed|Transaction Confirmed state">Confirmed</td>
|
||||||
<td><app-time kind="span" [time]="tx.status.block_time - transactionTime" [fastRender]="true" [relative]="true"></app-time></td>
|
<td><app-time-span [time]="tx.status.block_time - transactionTime" [fastRender]="true"></app-time-span></td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet' && featuresEnabled">
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
<td class="td-width" i18n="transaction.features|Transaction features">Features</td>
|
<td class="td-width" i18n="transaction.features|Transaction features">Features</td>
|
||||||
<td>
|
<td>
|
||||||
<app-tx-features [tx]="tx"></app-tx-features>
|
<app-tx-features [tx]="tx"></app-tx-features>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
<ng-template #firstSeenTmpl>
|
<ng-template #firstSeenTmpl>
|
||||||
<tr>
|
<tr>
|
||||||
<td i18n="transaction.first-seen|Transaction first seen">First seen</td>
|
<td i18n="transaction.first-seen|Transaction first seen">First seen</td>
|
||||||
<td><i><app-time kind="since" [time]="transactionTime" [fastRender]="true"></app-time></i></td>
|
<td><i><app-time-since [time]="transactionTime" [fastRender]="true"></app-time-since></i></td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -116,10 +116,10 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #belowBlockLimit>
|
<ng-template #belowBlockLimit>
|
||||||
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeEstimateDefault">
|
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeEstimateDefault">
|
||||||
<app-time kind="until" [time]="(60 * 1000 * txInBlockIndex) + now" [fastRender]="false" [fixedRender]="true"></app-time>
|
<app-time-until [time]="(60 * 1000 * txInBlockIndex) + now" [fastRender]="false" [fixedRender]="true"></app-time-until>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #timeEstimateDefault>
|
<ng-template #timeEstimateDefault>
|
||||||
<app-time kind="until" *ngIf="(timeAvg$ | async) as timeAvg;" [time]="(timeAvg * txInBlockIndex) + now + timeAvg" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time>
|
<app-time-until *ngIf="(timeAvg$ | async) as timeAvg;" [time]="(timeAvg * txInBlockIndex) + now + timeAvg" [fastRender]="false" [fixedRender]="true" [forceFloorOnTimeIntervals]="['hour']"></app-time-until>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@@ -210,7 +210,6 @@
|
|||||||
<div class="graph-container" #graphContainer>
|
<div class="graph-container" #graphContainer>
|
||||||
<tx-bowtie-graph
|
<tx-bowtie-graph
|
||||||
[tx]="tx"
|
[tx]="tx"
|
||||||
[cached]="isCached"
|
|
||||||
[width]="graphWidth"
|
[width]="graphWidth"
|
||||||
[height]="graphHeight"
|
[height]="graphHeight"
|
||||||
[lineLimit]="inOutLimit"
|
[lineLimit]="inOutLimit"
|
||||||
@@ -251,7 +250,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<app-transactions-list #txList [transactions]="[tx]" [cached]="isCached" [errorUnblinded]="errorUnblinded" [inputIndex]="inputIndex" [outputIndex]="outputIndex" [transactionPage]="true"></app-transactions-list>
|
<app-transactions-list #txList [transactions]="[tx]" [errorUnblinded]="errorUnblinded" [inputIndex]="inputIndex" [outputIndex]="outputIndex" [transactionPage]="true"></app-transactions-list>
|
||||||
|
|
||||||
<div class="title text-left">
|
<div class="title text-left">
|
||||||
<h2 i18n="transaction.details">Details</h2>
|
<h2 i18n="transaction.details">Details</h2>
|
||||||
@@ -468,7 +467,6 @@
|
|||||||
<ng-template #feeTable>
|
<ng-template #feeTable>
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngIf="isMobile && (network === 'liquid' || network === 'liquidtestnet' || !featuresEnabled)"></tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||||
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="blockConversion" [value]="tx.fee"></app-fiat></span></td>
|
<td>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [blockConversion]="blockConversion" [value]="tx.fee"></app-fiat></span></td>
|
||||||
@@ -489,7 +487,7 @@
|
|||||||
<div class="effective-fee-container">
|
<div class="effective-fee-container">
|
||||||
{{ tx.effectiveFeePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
{{ tx.effectiveFeePerVsize | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
|
||||||
<ng-template [ngIf]="tx.status.confirmed">
|
<ng-template [ngIf]="tx.status.confirmed">
|
||||||
<app-tx-fee-rating class="ml-2 mr-2" *ngIf="tx.fee || tx.effectiveFeePerVsize" [tx]="tx"></app-tx-fee-rating>
|
<app-tx-fee-rating class="d-none d-lg-inline ml-2" *ngIf="tx.fee" [tx]="tx"></app-tx-fee-rating>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">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" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { BlockExtended, CpfpInfo } from '../../interfaces/node-api.interface';
|
|||||||
import { LiquidUnblinding } from './liquid-ublinding';
|
import { LiquidUnblinding } from './liquid-ublinding';
|
||||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { Price, PriceService } from '../../services/price.service';
|
import { Price, PriceService } from '../../services/price.service';
|
||||||
import { isFeatureActive } from '../../bitcoin.utils';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-transaction',
|
selector: 'app-transaction',
|
||||||
@@ -58,7 +57,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
fetchCpfp$ = new Subject<string>();
|
fetchCpfp$ = new Subject<string>();
|
||||||
fetchRbfHistory$ = new Subject<string>();
|
fetchRbfHistory$ = new Subject<string>();
|
||||||
fetchCachedTx$ = new Subject<string>();
|
fetchCachedTx$ = new Subject<string>();
|
||||||
isCached: boolean = false;
|
|
||||||
now = new Date().getTime();
|
now = new Date().getTime();
|
||||||
timeAvg$: Observable<number>;
|
timeAvg$: Observable<number>;
|
||||||
liquidUnblinding = new LiquidUnblinding();
|
liquidUnblinding = new LiquidUnblinding();
|
||||||
@@ -75,12 +73,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
flowEnabled: boolean;
|
flowEnabled: boolean;
|
||||||
blockConversion: Price;
|
blockConversion: Price;
|
||||||
tooltipPosition: { x: number, y: number };
|
tooltipPosition: { x: number, y: number };
|
||||||
isMobile: boolean;
|
|
||||||
|
|
||||||
featuresEnabled: boolean;
|
|
||||||
segwitEnabled: boolean;
|
|
||||||
rbfEnabled: boolean;
|
|
||||||
taprootEnabled: boolean;
|
|
||||||
|
|
||||||
@ViewChild('graphContainer')
|
@ViewChild('graphContainer')
|
||||||
graphContainer: ElementRef;
|
graphContainer: ElementRef;
|
||||||
@@ -204,8 +196,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.tx = tx;
|
this.tx = tx;
|
||||||
this.setFeatures();
|
|
||||||
this.isCached = true;
|
|
||||||
if (tx.fee === undefined) {
|
if (tx.fee === undefined) {
|
||||||
this.tx.fee = 0;
|
this.tx.fee = 0;
|
||||||
}
|
}
|
||||||
@@ -299,8 +289,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.tx = tx;
|
this.tx = tx;
|
||||||
this.setFeatures();
|
|
||||||
this.isCached = false;
|
|
||||||
if (tx.fee === undefined) {
|
if (tx.fee === undefined) {
|
||||||
this.tx.fee = 0;
|
this.tx.fee = 0;
|
||||||
}
|
}
|
||||||
@@ -356,7 +344,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
|
this.blocksSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
|
||||||
this.latestBlock = block;
|
this.latestBlock = block;
|
||||||
|
|
||||||
if (txConfirmed && this.tx && !this.tx.status.confirmed) {
|
if (txConfirmed && this.tx) {
|
||||||
this.tx.status = {
|
this.tx.status = {
|
||||||
confirmed: true,
|
confirmed: true,
|
||||||
block_height: block.height,
|
block_height: block.height,
|
||||||
@@ -374,6 +362,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.waitingForTransaction = false;
|
this.waitingForTransaction = false;
|
||||||
}
|
}
|
||||||
this.rbfTransaction = rbfTransaction;
|
this.rbfTransaction = rbfTransaction;
|
||||||
|
this.cacheService.setTxCache([this.rbfTransaction]);
|
||||||
this.replaced = true;
|
this.replaced = true;
|
||||||
if (rbfTransaction && !this.tx) {
|
if (rbfTransaction && !this.tx) {
|
||||||
this.fetchCachedTx$.next(this.txId);
|
this.fetchCachedTx$.next(this.txId);
|
||||||
@@ -437,23 +426,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setFeatures(): void {
|
|
||||||
if (this.tx) {
|
|
||||||
this.segwitEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'segwit');
|
|
||||||
this.taprootEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'taproot');
|
|
||||||
this.rbfEnabled = !this.tx.status.confirmed || isFeatureActive(this.stateService.network, this.tx.status.block_height, 'rbf');
|
|
||||||
} else {
|
|
||||||
this.segwitEnabled = false;
|
|
||||||
this.taprootEnabled = false;
|
|
||||||
this.rbfEnabled = false;
|
|
||||||
}
|
|
||||||
this.featuresEnabled = this.segwitEnabled || this.taprootEnabled || this.rbfEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
resetTransaction() {
|
resetTransaction() {
|
||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
this.tx = null;
|
this.tx = null;
|
||||||
this.setFeatures();
|
|
||||||
this.waitingForTransaction = false;
|
this.waitingForTransaction = false;
|
||||||
this.isLoadingTx = true;
|
this.isLoadingTx = true;
|
||||||
this.rbfTransaction = undefined;
|
this.rbfTransaction = undefined;
|
||||||
@@ -518,11 +493,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
|
|
||||||
@HostListener('window:resize', ['$event'])
|
@HostListener('window:resize', ['$event'])
|
||||||
setGraphSize(): void {
|
setGraphSize(): void {
|
||||||
this.isMobile = window.innerWidth < 850;
|
|
||||||
if (this.graphContainer) {
|
if (this.graphContainer) {
|
||||||
setTimeout(() => {
|
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
|
||||||
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
|
|
||||||
}, 1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user