Compare commits

...

68 Commits

Author SHA1 Message Date
wiz
1efac916b7 Merge pull request #1800 from mempool/nymkappa/bugfix/missing-cache-version
Re-added missing cache version
2022-06-04 00:25:38 +09:00
wiz
3202629c44 Merge pull request #1772 from mempool/nymkappa/feature/websocket-block-count
Refactor pool ranking rxjs observable
2022-06-04 00:08:24 +09:00
nymkappa
3bc55d80ce Re-added missing cache version 2022-06-03 17:04:52 +02:00
wiz
89699f9b7e Merge branch 'master' into nymkappa/feature/websocket-block-count 2022-06-03 23:56:52 +09:00
wiz
95dd436be5 Merge pull request #1795 from hunicus/blocks-extras-v2
Update /api/blocks docs
2022-06-03 23:56:38 +09:00
wiz
efede07b5c Merge branch 'master' into blocks-extras-v2 2022-06-03 23:47:43 +09:00
wiz
a8123cddf7 Merge pull request #1792 from mempool/wiz/update-v2.4.0-screenshot
Update dashboard screenshot for v2.4.0 release
2022-06-03 23:47:21 +09:00
wiz
7764cceb86 Merge pull request #1797 from mempool/nymkappa/feature/automatic-disk-cache-reset
Skip blocks disk cache loading after db migration
2022-06-03 23:46:59 +09:00
nymkappa
256dbc8c8e Add disk cache versioning 2022-06-03 13:31:45 +02:00
hunicus
9ff006e61e Update /api/blocks in docs (bisq) 2022-06-02 18:08:09 -04:00
hunicus
9de6c716b7 Update /api/blocks in docs (liquid) 2022-06-02 17:51:23 -04:00
hunicus
538a1b1666 Replace blocks-extras with blocks in docs (bitcoin) 2022-06-02 17:29:13 -04:00
nymkappa
56e996c893 Refactor pool block list observable 2022-06-02 22:01:53 +02:00
nymkappa
429b4f2bc6 Refactor pool ranking rxjs observable 2022-06-02 22:01:53 +02:00
softsimon
3196c188f1 Merge pull request #1794 from mempool/simon/transifex-extract-6-2
Extracting i18n strings
2022-06-02 22:42:50 +04:00
softsimon
672833930d Extracting i18n strings 2022-06-02 22:42:27 +04:00
wiz
74ee35e273 Merge pull request #1793 from mempool/wiz/update-about-page-logos-for-v2.4
Update the Enterprise Sponsor logos on About page
2022-06-03 03:35:19 +09:00
wiz
8095a8a5f5 Fix size of Exodus logo on About page 2022-06-03 03:27:00 +09:00
wiz
ed3a614fb7 Change Gemini logo on About page to dark theme 2022-06-03 03:26:42 +09:00
wiz
cabfdcf49c Re-arrange Enterprise Sponsor logos on About page 2022-06-03 03:17:58 +09:00
wiz
69e1474c53 Update Exodus logo on About page 2022-06-03 03:01:12 +09:00
wiz
11f5056871 Merge pull request #1784 from hunicus/node-version-requirement
Make node 16.15 required not recommended
2022-06-03 02:53:55 +09:00
wiz
10ccad16e9 Update dashboard screenshot for v2.4.0 release
Fixes #1778
2022-06-03 02:48:38 +09:00
wiz
18c1be0bd0 Merge pull request #1790 from mempool/nymkappa/feature/fee-redesign-2
Updated new fee widget design
2022-06-03 01:23:58 +09:00
nymkappa
fe32ef75a2 Updated new fee widget design 2022-06-02 17:44:44 +02:00
wiz
a6517ebdc5 Merge pull request #1779 from mempool/simon/projected-block-transactions-fixes
Minor refactor for projected block transactions
2022-06-02 21:30:57 +09:00
hunicus
83660e9cf3 Make node 16.15 required not recommended 2022-06-01 22:54:46 -04:00
softsimon
f0a2ddf57b Minor refactor for projected block transactions 2022-06-02 01:29:03 +04:00
wiz
ddab579111 Merge pull request #1774 from mononaut/projected-block-overview
Feature: Projected block visualization
2022-06-02 05:35:07 +09:00
wiz
82471073c3 Merge branch 'master' into projected-block-overview 2022-06-02 00:35:07 +09:00
wiz
c9b98ed841 Merge pull request #1766 from mempool/nymkappa/feature/fee-redesign
Rewamp the fee widget
2022-06-02 00:34:57 +09:00
Mononaut
57cecee3af Add contributors agreement for mononaut 2022-06-01 13:48:58 +00:00
Mononaut
6cd8c448b4 Projected block loading spinner & WebGL detection 2022-06-01 13:48:58 +00:00
Mononaut
3ffc4956f4 Stream projected block deltas instead of full data 2022-06-01 13:48:58 +00:00
Mononaut
c2802253b7 Smarter update algorithm for projected block viz 2022-06-01 13:48:35 +00:00
Mononaut
1aac96a6f6 Projected block overview mouse events & tx preview 2022-06-01 13:48:34 +00:00
Mononaut
d4c9f6decb Implement WebGL projected block visualization 2022-06-01 13:48:34 +00:00
Mononaut
79dae84363 Data pipeline for projected mempool block overview 2022-06-01 13:48:34 +00:00
nymkappa
34576c0609 Update gradient as soon as we receive the fees 2022-06-01 12:53:36 +02:00
nymkappa
ec24549602 Hide minimum and economy on mobile 2022-06-01 12:28:42 +02:00
wiz
7f8834a2eb Merge branch 'master' into nymkappa/feature/fee-redesign 2022-06-01 17:22:01 +09:00
wiz
ee5cd1cd96 Merge pull request #1769 from mempool/nymkappa/bugfix/divergence-fix
Re-add hash field in the mysql block query
2022-06-01 17:20:38 +09:00
wiz
9ab3b3293a Merge pull request #1765 from mempool/simon/init-data-fees
Send fee info with init data
2022-06-01 17:08:09 +09:00
nymkappa
d860344be4 Re-add hash field in the mysql block query 2022-06-01 10:06:18 +02:00
nymkappa
cefc927b06 Tweaks fee widget 2022-06-01 09:47:00 +02:00
wiz
72cc2e4df0 Merge branch 'master' into nymkappa/feature/fee-redesign 2022-06-01 15:14:56 +09:00
nymkappa
b4fd98f565 Rewamp the fee widget 2022-05-31 22:31:01 +02:00
softsimon
2ee1f197d1 Send fee info with init data 2022-06-01 00:03:25 +04:00
wiz
e629173304 Merge pull request #1642 from mempool/nymkappa/bugfix/bisq-dump-file
If bisq data folder is not ready, retry every 3 minutes instead of exit
2022-06-01 03:58:24 +09:00
wiz
6210936ef4 Merge pull request #1752 from mempool/wiz/use-svg-logo-for-mempool.space
Use inline SVG for mempool.space logo
2022-06-01 03:52:40 +09:00
wiz
de8a51fe9a Merge branch 'master' into nymkappa/bugfix/bisq-dump-file 2022-06-01 03:47:12 +09:00
wiz
260f883d02 Merge pull request #1764 from mempool/nymkappa/bugfix/fx-rate-db-disable
Only attempt to save fx rate if database is enabled
2022-06-01 03:47:05 +09:00
softsimon
e81dfbcc7f Adding missing ngIf check 2022-05-31 22:39:15 +04:00
nymkappa
c7e9b47aa0 Only attempt to save fx rate if database is enabled 2022-05-31 20:29:43 +02:00
wiz
7f6ea58c74 Merge branch 'master' into wiz/use-svg-logo-for-mempool.space 2022-06-01 03:22:52 +09:00
softsimon
5dcde1c702 Store SVG images in a separate component 2022-05-31 22:21:59 +04:00
wiz
4c90d8e811 Merge branch 'master' into nymkappa/bugfix/bisq-dump-file 2022-06-01 03:10:50 +09:00
wiz
dbce727695 Merge pull request #1741 from mempool/nymkappa/feature/remove-fee-calculation-frontend
Remove fee calculation from the frontend
2022-06-01 03:03:09 +09:00
wiz
39f33dded2 Merge branch 'master' into nymkappa/feature/remove-fee-calculation-frontend 2022-06-01 02:44:48 +09:00
wiz
4b445b1191 Merge pull request #1763 from mempool/wiz/fix-backend-starting-syslog-msg
Change backend start syslog message from DEBUG to NOTICE
2022-06-01 02:43:25 +09:00
wiz
a9515f8fc1 Merge pull request #1751 from mempool/simon/handle-nonstandard-inputs
Handle nonstandard inputs
2022-06-01 02:42:57 +09:00
wiz
4ccd786fe9 Change backend start syslog message from DEBUG to NOTICE 2022-06-01 01:09:08 +09:00
wiz
72c3eea863 Merge branch 'master' into nymkappa/bugfix/bisq-dump-file 2022-06-01 00:14:43 +09:00
wiz
6b2b10960a Merge branch 'master' into nymkappa/feature/remove-fee-calculation-frontend 2022-05-31 18:34:49 +09:00
nymkappa
f20cf266b6 Remove frontend fee calculation and read it from the websocket instead 2022-05-31 10:41:43 +02:00
wiz
82161d4edf Use SVG logo for mempool.space 2022-05-31 17:31:31 +09:00
softsimon
85b17927d6 Handle nonstandard inputs
fixes #1744
2022-05-31 04:13:13 +04:00
nymkappa
7e8e4b1e6c If bisq data folder is not ready, retry every 3 minutes instead of exit 2022-05-13 11:54:52 +02:00
48 changed files with 2713 additions and 600 deletions

View File

@@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
#### Build
_Node.js 16 and npm 7 are recommended._
_Make sure to use Node.js 16.15 and npm 7._
Install dependencies with `npm` and build the backend:

View File

@@ -35,7 +35,13 @@ class Bisq {
constructor() {}
startBisqService(): void {
this.checkForBisqDataFolder();
try {
this.checkForBisqDataFolder();
} catch (e) {
logger.info('Retrying to start bisq service in 3 minutes');
setTimeout(this.startBisqService.bind(this), 180000);
return;
}
this.loadBisqDumpFile();
setInterval(this.updatePrice.bind(this), 1000 * 60 * 60);
this.updatePrice();
@@ -90,7 +96,7 @@ class Bisq {
private checkForBisqDataFolder() {
if (!fs.existsSync(Bisq.BLOCKS_JSON_FILE_PATH)) {
logger.warn(Bisq.BLOCKS_JSON_FILE_PATH + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`);
return process.exit(1);
throw new Error(`Cannot load BISQ ${Bisq.BLOCKS_JSON_FILE_PATH} file`);
}
}
@@ -162,7 +168,7 @@ class Bisq {
this.buildIndex();
this.calculateStats();
} catch (e) {
logger.info('loadBisqDumpFile() error.' + (e instanceof Error ? e.message : e));
logger.info('Cannot load bisq dump file because: ' + (e instanceof Error ? e.message : e));
}
}

View File

@@ -26,7 +26,13 @@ class Bisq {
constructor() {}
startBisqService(): void {
this.checkForBisqDataFolder();
try {
this.checkForBisqDataFolder();
} catch (e) {
logger.info('Retrying to start bisq service (markets) in 3 minutes');
setTimeout(this.startBisqService.bind(this), 180000);
return;
}
this.loadBisqDumpFile();
this.startBisqDirectoryWatcher();
}
@@ -34,7 +40,7 @@ class Bisq {
private checkForBisqDataFolder() {
if (!fs.existsSync(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency)) {
logger.err(Bisq.MARKET_JSON_PATH + Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency + ` doesn't exist. Make sure Bisq is running and the config is correct before starting the server.`);
return process.exit(1);
throw new Error(`Cannot load BISQ ${Bisq.MARKET_JSON_FILE_PATHS.cryptoCurrency} file`);
}
}

View File

@@ -343,7 +343,7 @@ class Blocks {
await blocksRepository.$saveBlockInDatabase(blockExtended);
}
}
if (fiatConversion.ratesInitialized === true) {
if (fiatConversion.ratesInitialized === true && config.DATABASE.ENABLED === true) {
await RatesRepository.$saveRate(blockExtended.height, fiatConversion.getConversionRates());
}

View File

@@ -9,6 +9,8 @@ import { TransactionExtended } from '../mempool.interfaces';
import { Common } from './common';
class DiskCache {
private cacheSchemaVersion = 1;
private static FILE_NAME = config.MEMPOOL.CACHE_DIR + '/cache.json';
private static FILE_NAMES = config.MEMPOOL.CACHE_DIR + '/cache{number}.json';
private static CHUNK_FILES = 25;
@@ -39,6 +41,7 @@ class DiskCache {
const chunkSize = Math.floor(mempoolArray.length / DiskCache.CHUNK_FILES);
await fsPromises.writeFile(DiskCache.FILE_NAME, JSON.stringify({
cacheSchemaVersion: this.cacheSchemaVersion,
blocks: blocks.getBlocks(),
mempool: {},
mempoolArray: mempoolArray.splice(0, chunkSize),
@@ -57,6 +60,13 @@ class DiskCache {
}
}
wipeCache() {
fs.unlinkSync(DiskCache.FILE_NAME);
for (let i = 1; i < DiskCache.CHUNK_FILES; i++) {
fs.unlinkSync(DiskCache.FILE_NAMES.replace('{number}', i.toString()));
}
}
loadMempoolCache() {
if (!fs.existsSync(DiskCache.FILE_NAME)) {
return;
@@ -67,6 +77,11 @@ class DiskCache {
if (cacheData) {
logger.info('Restoring mempool and blocks data from disk cache');
data = JSON.parse(cacheData);
if (data.cacheSchemaVersion === undefined || data.cacheSchemaVersion !== this.cacheSchemaVersion) {
logger.notice('Disk cache contains an outdated schema version. Clearing it and skipping the cache loading.');
return this.wipeCache();
}
if (data.mempoolArray) {
for (const tx of data.mempoolArray) {
data.mempool[tx.txid] = tx;

View File

@@ -1,10 +1,11 @@
import logger from '../logger';
import { MempoolBlock, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
import { MempoolBlock, TransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta } from '../mempool.interfaces';
import { Common } from './common';
import config from '../config';
class MempoolBlocks {
private mempoolBlocks: MempoolBlockWithTransactions[] = [];
private mempoolBlockDeltas: MempoolBlockDelta[] = [];
constructor() {}
@@ -25,6 +26,10 @@ class MempoolBlocks {
return this.mempoolBlocks;
}
public getMempoolBlockDeltas(): MempoolBlockDelta[] {
return this.mempoolBlockDeltas;
}
public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }): void {
const latestMempool = memPool;
const memPoolArray: TransactionExtended[] = [];
@@ -66,11 +71,15 @@ class MempoolBlocks {
const time = end - start;
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
this.mempoolBlocks = this.calculateMempoolBlocks(memPoolArray);
const { blocks, deltas } = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks);
this.mempoolBlocks = blocks;
this.mempoolBlockDeltas = deltas;
}
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[]): MempoolBlockWithTransactions[] {
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]):
{ blocks: MempoolBlockWithTransactions[], deltas: MempoolBlockDelta[] } {
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
const mempoolBlockDeltas: MempoolBlockDelta[] = [];
let blockWeight = 0;
let blockSize = 0;
let transactions: TransactionExtended[] = [];
@@ -90,7 +99,43 @@ class MempoolBlocks {
if (transactions.length) {
mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length));
}
return mempoolBlocks;
// Calculate change from previous block states
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
let added: TransactionStripped[] = [];
let removed: string[] = [];
if (mempoolBlocks[i] && !prevBlocks[i]) {
added = mempoolBlocks[i].transactions;
} else if (!mempoolBlocks[i] && prevBlocks[i]) {
removed = prevBlocks[i].transactions.map(tx => tx.txid);
} else if (mempoolBlocks[i] && prevBlocks[i]) {
const prevIds = {};
const newIds = {};
prevBlocks[i].transactions.forEach(tx => {
prevIds[tx.txid] = true;
});
mempoolBlocks[i].transactions.forEach(tx => {
newIds[tx.txid] = true;
});
prevBlocks[i].transactions.forEach(tx => {
if (!newIds[tx.txid]) {
removed.push(tx.txid);
}
});
mempoolBlocks[i].transactions.forEach(tx => {
if (!prevIds[tx.txid]) {
added.push(tx);
}
});
}
mempoolBlockDeltas.push({
added,
removed
});
}
return {
blocks: mempoolBlocks,
deltas: mempoolBlockDeltas
};
}
private dataToMempoolBlocks(transactions: TransactionExtended[],
@@ -112,6 +157,7 @@ class MempoolBlocks {
medianFee: Common.percentile(transactions.map((tx) => tx.effectiveFeePerVsize), config.MEMPOOL.RECOMMENDED_FEE_PERCENTILE),
feeRange: Common.getFeesInRange(transactions, rangeLength),
transactionIds: transactions.map((tx) => tx.txid),
transactions: transactions.map((tx) => Common.stripTransaction(tx)),
};
}
}

View File

@@ -1,6 +1,6 @@
import logger from '../logger';
import * as WebSocket from 'ws';
import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock,
import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, MempoolBlockDelta,
OptimizedStatistic, ILoadingIndicators, IConversionRates } from '../mempool.interfaces';
import blocks from './blocks';
import memPool from './mempool';
@@ -13,6 +13,7 @@ import config from '../config';
import transactionUtils from './transaction-utils';
import rbfCache from './rbf-cache';
import difficultyAdjustment from './difficulty-adjustment';
import feeApi from './fee-api';
class WebsocketHandler {
private wss: WebSocket.Server | undefined;
@@ -110,6 +111,22 @@ class WebsocketHandler {
}
}
if (parsedMessage && parsedMessage['track-mempool-block'] !== undefined) {
if (Number.isInteger(parsedMessage['track-mempool-block']) && parsedMessage['track-mempool-block'] >= 0) {
const index = parsedMessage['track-mempool-block'];
client['track-mempool-block'] = index;
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
if (mBlocksWithTransactions[index]) {
response['projected-block-transactions'] = {
index: index,
blockTransactions: mBlocksWithTransactions[index].transactions
};
}
} else {
client['track-mempool-block'] = null;
}
}
if (parsedMessage.action === 'init') {
const _blocks = blocks.getBlocks().slice(-config.MEMPOOL.INITIAL_BLOCKS_AMOUNT);
if (!_blocks) {
@@ -199,6 +216,7 @@ class WebsocketHandler {
'backendInfo': backendInfo.getBackendInfo(),
'loadingIndicators': loadingIndicators.getLoadingIndicators(),
'da': difficultyAdjustment.getDifficultyAdjustment(),
'fees': feeApi.getRecommendedFee(),
...this.extraInitProperties
};
}
@@ -231,11 +249,13 @@ class WebsocketHandler {
mempoolBlocks.updateMempoolBlocks(newMempool);
const mBlocks = mempoolBlocks.getMempoolBlocks();
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
const mempoolInfo = memPool.getMempoolInfo();
const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
const da = difficultyAdjustment.getDifficultyAdjustment();
memPool.handleRbfTransactions(rbfTransactions);
const recommendedFees = feeApi.getRecommendedFee();
this.wss.clients.forEach(async (client: WebSocket) => {
if (client.readyState !== WebSocket.OPEN) {
@@ -249,6 +269,7 @@ class WebsocketHandler {
response['vBytesPerSecond'] = vBytesPerSecond;
response['transactions'] = newTransactions.slice(0, 6).map((tx) => Common.stripTransaction(tx));
response['da'] = da;
response['fees'] = recommendedFees;
}
if (client['want-mempool-blocks']) {
@@ -366,6 +387,16 @@ class WebsocketHandler {
}
}
if (client['track-mempool-block'] >= 0) {
const index = client['track-mempool-block'];
if (mBlockDeltas[index]) {
response['projected-block-transactions'] = {
index: index,
delta: mBlockDeltas[index],
};
}
}
if (Object.keys(response).length) {
client.send(JSON.stringify(response));
}
@@ -378,6 +409,7 @@ class WebsocketHandler {
}
let mBlocks: undefined | MempoolBlock[];
let mBlockDeltas: undefined | MempoolBlockDelta[];
let matchRate = 0;
const _memPool = memPool.getMempool();
const _mempoolBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
@@ -394,12 +426,16 @@ class WebsocketHandler {
matchRate = Math.round((matches.length / (txIds.length - 1)) * 100);
mempoolBlocks.updateMempoolBlocks(_memPool);
mBlocks = mempoolBlocks.getMempoolBlocks();
mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
}
if (block.extras) {
block.extras.matchRate = matchRate;
}
const da = difficultyAdjustment.getDifficultyAdjustment();
const fees = feeApi.getRecommendedFee();
this.wss.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
@@ -412,7 +448,8 @@ class WebsocketHandler {
const response = {
'block': block,
'mempoolInfo': memPool.getMempoolInfo(),
'da': difficultyAdjustment.getDifficultyAdjustment(),
'da': da,
'fees': fees,
};
if (mBlocks && client['want-mempool-blocks']) {
@@ -487,6 +524,16 @@ class WebsocketHandler {
}
}
if (client['track-mempool-block'] >= 0) {
const index = client['track-mempool-block'];
if (mBlockDeltas && mBlockDeltas[index]) {
response['projected-block-transactions'] = {
index: index,
delta: mBlockDeltas[index],
};
}
}
client.send(JSON.stringify(response));
});
}

View File

@@ -67,7 +67,7 @@ class Server {
}
async startServer(worker = false) {
logger.debug(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
this.app
.use((req: Request, res: Response, next: NextFunction) => {

View File

@@ -33,6 +33,12 @@ export interface MempoolBlock {
export interface MempoolBlockWithTransactions extends MempoolBlock {
transactionIds: string[];
transactions: TransactionStripped[];
}
export interface MempoolBlockDelta {
added: TransactionStripped[];
removed: string[];
}
interface VinStrippedToScriptsig {

View File

@@ -307,6 +307,7 @@ class BlocksRepository {
try {
const [rows]: any[] = await DB.query(`SELECT
height,
hash,
hash as id,
UNIX_TIMESTAMP(blocks.blockTimestamp) as blockTimestamp,
size,

View File

@@ -0,0 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of May 31, 2022.
Signed: mononaut

View File

@@ -34,7 +34,7 @@ $ npm run config:defaults:bisq
### 3. Run the Frontend
_Node.js 16 and npm 7 are recommended._
_Make sure to use Node.js 16.15 and npm 7._
Install project dependencies and run the frontend server:

View File

@@ -47,32 +47,6 @@
</svg>
<span>Spiral</span>
</a>
<a href="https://gemini.com/" target="_blank" title="Gemini">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
<rect style="fill: white" width="360" height="360" />
<g transform="matrix(0.62 0 0 0.62 180 180)">
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</svg>
<span>Gemini</span>
</a>
<a href="https://exodus.com/" target="_blank" title="Exodus">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400px" height="400px" viewBox="0 0 400 400" class="image">
<defs>
<linearGradient x1="0%" y1="50%" x2="100%" y2="50%" id="linearGradient-1">
<stop stop-color="#00BFFF" offset="0%"></stop>
<stop stop-color="#6619FF" offset="100%"></stop>
</linearGradient>
</defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g>
<rect fill="#1A1D40" x="0" y="0" width="400" height="400"></rect>
<path d="M244.25,200 L310,265.75 L286.8,265.75 C282.823093,265.746499 279.010347,264.16385 276.2,261.35 L215,200 L276.25,138.6 C279.068515,135.804479 282.880256,134.240227 286.85,134.249954 L310,134.249954 L244.25,200 Z M123.75,138.6 C120.931485,135.804479 117.119744,134.240227 113.15,134.249954 L90,134.249954 L155.75,200 L90,265.75 L113.2,265.75 C117.176907,265.746499 120.989653,264.16385 123.8,261.35 L185,200 L123.75,138.6 Z M200,215 L138.6,276.25 C135.804479,279.068515 134.240227,282.880256 134.249954,286.85 L134.249954,310 L200,244.25 L265.750046,310 L265.750046,286.85 C265.759773,282.880256 264.195521,279.068515 261.4,276.25 L200,215 Z M200,185 L261.4,123.75 C264.195521,120.931485 265.759773,117.119744 265.750046,113.15 L265.750046,90 L200,155.75 L134.249954,90 L134.249954,113.15 C134.240227,117.119744 135.804479,120.931485 138.6,123.75 L200,185 Z" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
</g>
</g>
</svg>
<span>Exodus</span>
</a>
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="400px" height="400px" viewBox="0 0 400 400" class="image">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
@@ -84,25 +58,6 @@
</svg>
<span>Foundry</span>
</a>
<a href="https://unchained.com/" target="_blank" title="Unchained">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 216 216" class="image" style="enable-background:new 0 0 216 216;">
<style type="text/css">
.ucst0{fill:#002248;}
.ucst1{opacity:0.5;fill:#FFFFFF;}
.ucst2{fill:#FFFFFF;}
.ucst3{opacity:0.75;fill:#FFFFFF;}
</style>
<rect class="ucst0" width="216" height="216"/>
<g>
<g>
<path class="ucst1" d="M108,39.5V108l59.3,34.2V73.8L108,39.5z M126.9,95.4c0,2,1.1,3.8,2.8,4.8l27.9,16l0,10.8L125,108.2c-4.6-2.6-7.4-7.5-7.4-12.8l-0.1-22.7c0-1.9,0.5-3.7,1.4-5.3c0.9-1.5,2.2-2.9,3.8-3.8c3.3-1.9,7.2-1.9,10.5,0l24.5,14.2l-0.2,10.7l-29-16.8c-0.5-0.3-0.9-0.2-1.2,0c-0.3,0.2-0.6,0.5-0.6,1L126.9,95.4z"/>
<path class="ucst2" d="M108,39.5L48.7,73.8v68.5L108,108V39.5z M99.7,93.1c0,5.3-2.8,10.2-7.4,12.8l-19.6,11.4c-1.7,1-3.5,1.4-5.3,1.5c-1.8,0-3.6-0.5-5.2-1.4c-3.3-1.9-5.3-5.3-5.3-9.1V80l9.4-5.2l-0.1,33.5c0,0.6,0.3,0.9,0.6,1c0.3,0.2,0.7,0.3,1.2,0l19.6-11.4c1.7-1,2.8-2.8,2.8-4.8L90.3,61l9.4-5.4L99.7,93.1z"/>
<path class="ucst3" d="M108,108l-59.3,34.2l59.3,34.2l59.3-34.2L108,108z M133.8,152l-24.5,14.2l-9.2-5.5l29.1-16.7c0.5-0.3,0.6-0.7,0.6-1c0-0.3-0.1-0.7-0.6-1l-19.7-11.2c-1.7-1-3.8-1-5.5,0l-27.8,16.1l-9.4-5.4l32.6-18.7c4.6-2.6,10.2-2.6,14.8,0l19.7,11.2c1.7,0.9,3,2.3,3.9,3.9c0.9,1.5,1.4,3.3,1.4,5.2C139.1,146.7,137.1,150.1,133.8,152z"/>
</g>
</g>
</svg>
<span>Unchained</span>
</a>
<a href="https://blockstream.com/" target="_blank" title="Blockstream">
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" x="0px" y="0px" viewBox="200 200 600 600" class="image" style="enable-background:new 0 0 1000 1000;background-color: #111316 !important">
<style type="text/css">
@@ -149,6 +104,76 @@
</svg>
<span>Blockstream</span>
</a>
<a href="https://unchained.com/" target="_blank" title="Unchained">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" x="0px" y="0px" viewBox="0 0 216 216" class="image" style="enable-background:new 0 0 216 216;">
<style type="text/css">
.ucst0{fill:#002248;}
.ucst1{opacity:0.5;fill:#FFFFFF;}
.ucst2{fill:#FFFFFF;}
.ucst3{opacity:0.75;fill:#FFFFFF;}
</style>
<rect class="ucst0" width="216" height="216"/>
<g>
<g>
<path class="ucst1" d="M108,39.5V108l59.3,34.2V73.8L108,39.5z M126.9,95.4c0,2,1.1,3.8,2.8,4.8l27.9,16l0,10.8L125,108.2c-4.6-2.6-7.4-7.5-7.4-12.8l-0.1-22.7c0-1.9,0.5-3.7,1.4-5.3c0.9-1.5,2.2-2.9,3.8-3.8c3.3-1.9,7.2-1.9,10.5,0l24.5,14.2l-0.2,10.7l-29-16.8c-0.5-0.3-0.9-0.2-1.2,0c-0.3,0.2-0.6,0.5-0.6,1L126.9,95.4z"/>
<path class="ucst2" d="M108,39.5L48.7,73.8v68.5L108,108V39.5z M99.7,93.1c0,5.3-2.8,10.2-7.4,12.8l-19.6,11.4c-1.7,1-3.5,1.4-5.3,1.5c-1.8,0-3.6-0.5-5.2-1.4c-3.3-1.9-5.3-5.3-5.3-9.1V80l9.4-5.2l-0.1,33.5c0,0.6,0.3,0.9,0.6,1c0.3,0.2,0.7,0.3,1.2,0l19.6-11.4c1.7-1,2.8-2.8,2.8-4.8L90.3,61l9.4-5.4L99.7,93.1z"/>
<path class="ucst3" d="M108,108l-59.3,34.2l59.3,34.2l59.3-34.2L108,108z M133.8,152l-24.5,14.2l-9.2-5.5l29.1-16.7c0.5-0.3,0.6-0.7,0.6-1c0-0.3-0.1-0.7-0.6-1l-19.7-11.2c-1.7-1-3.8-1-5.5,0l-27.8,16.1l-9.4-5.4l32.6-18.7c4.6-2.6,10.2-2.6,14.8,0l19.7,11.2c1.7,0.9,3,2.3,3.9,3.9c0.9,1.5,1.4,3.3,1.4,5.2C139.1,146.7,137.1,150.1,133.8,152z"/>
</g>
</g>
</svg>
<span>Unchained</span>
</a>
<a href="https://gemini.com/" target="_blank" title="Gemini">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
<rect style="fill: black" width="360" height="360" />
<g transform="matrix(0.62 0 0 0.62 180 180)">
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
</g>
</svg>
<span>Gemini</span>
</a>
<a href="https://exodus.com/" target="_blank" title="Exodus">
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
<g clip-path="url(#clip0_2_14)">
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
<path d="M150.638 271.648H229.168V229.351H150.346L140.128 196.097L229.168 138.048V87L89.9159 178.303L112.687 250.646L89.9159 322.989L229.459 414V362.952L140.128 304.903L150.638 271.648Z" fill="url(#paint1_linear_2_14)"/>
<mask id="mask0_2_14" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="89" y="87" width="323" height="327">
<path d="M411.042 178.303L271.79 87.0001V138.048L361.121 196.097L350.612 229.352H271.79V271.649H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint2_linear_2_14)"/>
<path d="M150.638 271.649H229.168V229.352H150.346L140.128 196.097L229.168 138.048V87.0001L89.9161 178.303L112.687 250.646L89.9161 322.989L229.46 414V362.952L140.128 304.903L150.638 271.649Z" fill="url(#paint3_linear_2_14)"/>
</mask>
<g mask="url(#mask0_2_14)">
<path d="M408.913 87.0001H90.0877V414H408.913V87.0001Z" fill="url(#paint4_linear_2_14)"/>
</g>
</g>
<defs>
<linearGradient id="paint0_linear_2_14" x1="365.994" y1="436.481" x2="272.717" y2="51.089" gradientUnits="userSpaceOnUse">
<stop stop-color="#0B46F9"/>
<stop offset="1" stop-color="#BBFBE0"/>
</linearGradient>
<linearGradient id="paint1_linear_2_14" x1="365.994" y1="436.481" x2="272.717" y2="51.089" gradientUnits="userSpaceOnUse">
<stop stop-color="#0B46F9"/>
<stop offset="1" stop-color="#BBFBE0"/>
</linearGradient>
<linearGradient id="paint2_linear_2_14" x1="365.994" y1="436.481" x2="272.717" y2="51.0891" gradientUnits="userSpaceOnUse">
<stop stop-color="#0B46F9"/>
<stop offset="1" stop-color="#BBFBE0"/>
</linearGradient>
<linearGradient id="paint3_linear_2_14" x1="365.994" y1="436.481" x2="272.717" y2="51.0891" gradientUnits="userSpaceOnUse">
<stop stop-color="#0B46F9"/>
<stop offset="1" stop-color="#BBFBE0"/>
</linearGradient>
<linearGradient id="paint4_linear_2_14" x1="110.525" y1="160.575" x2="271.982" y2="281.156" gradientUnits="userSpaceOnUse">
<stop offset="0.119792" stop-color="#8952FF" stop-opacity="0.87"/>
<stop offset="1" stop-color="#DABDFF" stop-opacity="0"/>
</linearGradient>
<clipPath id="clip0_2_14">
<rect width="327" height="327" fill="white" transform="translate(86 87)"/>
</clipPath>
</defs>
</svg>
<span>Exodus</span>
</a>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
import { BehaviorSubject, combineLatest, Observable, timer } from 'rxjs';
import { BehaviorSubject, combineLatest, concat, Observable, timer } from 'rxjs';
import { delayWhen, map, retryWhen, scan, skip, switchMap, tap } from 'rxjs/operators';
import { BlockExtended } from 'src/app/interfaces/node-api.interface';
import { ApiService } from 'src/app/services/api.service';
@@ -15,7 +15,7 @@ import { WebsocketService } from 'src/app/services/websocket.service';
export class BlocksList implements OnInit {
@Input() widget: boolean = false;
blocks$: Observable<any[]> = undefined;
blocks$: Observable<BlockExtended[]> = undefined;
indexingAvailable = false;
isLoading = true;
@@ -27,6 +27,7 @@ export class BlocksList implements OnInit {
blocksCount: number;
fromHeightSubject: BehaviorSubject<number> = new BehaviorSubject(this.fromBlockHeight);
skeletonLines: number[] = [];
lastBlockHeight = -1;
constructor(
private apiService: ApiService,
@@ -57,6 +58,7 @@ export class BlocksList implements OnInit {
this.blocksCount = blocks[0].height + 1;
}
this.isLoading = false;
this.lastBlockHeight = Math.max(...blocks.map(o => o.height))
}),
map(blocks => {
if (this.indexingAvailable) {
@@ -73,12 +75,18 @@ export class BlocksList implements OnInit {
}),
retryWhen(errors => errors.pipe(delayWhen(() => timer(10000))))
)
})
})
),
this.stateService.blocks$
.pipe(
skip(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT - 1),
),
switchMap((block) => {
if (block[0].height <= this.lastBlockHeight) {
return []; // Return an empty stream so the last pipe is not executed
}
this.lastBlockHeight = block[0].height;
return [block];
})
)
])
.pipe(
scan((acc, blocks) => {

View File

@@ -133,7 +133,7 @@
text-align: center;
flex-direction: column;
justify-content: space-around;
padding: 22px 20px;
padding: 24px 20px;
}
}
@@ -148,3 +148,7 @@
margin-right: -2px;
font-size: 10px;
}
.symbol {
font-size: 13px;
}

View File

@@ -1,44 +1,73 @@
<div class="fee-estimation-wrapper">
<div class="fee-estimation-container" *ngIf="(isLoadingWebSocket$ | async) === false && (feeEstimations$ | async) as feeEstimations; else loadingFees">
<div class="fee-estimation-wrapper" *ngIf="(isLoadingWebSocket$ | async) === false && (recommendedFees$ | async) as recommendedFees; else loadingFees">
<div class="d-flex">
<div class="fee-progress-bar" [style.background]="noPriority">
<span class="fee-label" i18n="fees-box.no-priority">No Priority</span>
</div>
<div class="band-separator fill"></div>
<div class="fee-progress-bar priority" [style.background]="gradient">
<span class="fee-label prority" i18n="fees-box.low-priority">Low Priority</span>
<span class="fee-label prority" i18n="fees-box.medium-priority">Medium Priority</span>
<span class="fee-label prority" i18n="fees-box.high-priority">High Priority</span>
</div>
</div>
<div class="fee-estimation-container">
<div class="item">
<h5 class="card-title" i18n="fees-box.low-priority">Low priority</h5>
<div class="card-text" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom">
<div class="fee-text">{{ feeEstimations.hourFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat [value]="feeEstimations.hourFee * 140" ></app-fiat></span>
<div class="fee-text">{{ recommendedFees.economyFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat [value]="recommendedFees.economyFee * 140" ></app-fiat></span>
</div>
</div>
<div class="band-separator"></div>
<div class="item">
<div class="card-text" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom">
<div class="fee-text">{{ recommendedFees.hourFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat [value]="recommendedFees.hourFee * 140" ></app-fiat></span>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="fees-box.medium-priority">Medium priority</h5>
<div class="card-text" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom">
<div class="fee-text">{{ feeEstimations.halfHourFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat [value]="feeEstimations.halfHourFee * 140" ></app-fiat></span>
<div class="fee-text">{{ recommendedFees.halfHourFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat [value]="recommendedFees.halfHourFee * 140" ></app-fiat></span>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="fees-box.high-priority">High priority</h5>
<div class="card-text" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom">
<div class="fee-text">{{ feeEstimations.fastestFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat [value]="feeEstimations.fastestFee * 140" ></app-fiat></span>
<div class="fee-text">{{ recommendedFees.fastestFee }} <span i18n="shared.sat-vbyte|sat/vB">sat/vB</span></div> <span class="fiat"><app-fiat [value]="recommendedFees.fastestFee * 140" ></app-fiat></span>
</div>
</div>
</div>
</div>
<ng-template #loadingFees>
<div class="d-flex">
<div class="fee-progress-bar" [style.background]="noPriority">
<span class="fee-label" i18n="fees-box.no-priority">No Priority</span>
</div>
<div class="band-separator fill"></div>
<div class="fee-progress-bar priority" [style.background]="gradient">
<span class="fee-label prority" i18n="fees-box.low-priority">Low Priority</span>
<span class="fee-label prority" i18n="fees-box.medium-priority">Medium Priority</span>
<span class="fee-label prority" i18n="fees-box.high-priority">High Priority</span>
</div>
</div>
<div class="fee-estimation-container loading-container">
<div class="item">
<h5 class="card-title" i18n="fees-box.low-priority">Low priority</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="band-separator"></div>
<div class="item">
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>
</div>
</div>
<div class="item">
<h5 class="card-title" i18n="fees-box.medium-priority">Medium priority</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="fees-box.high-priority">High priority</h5>
<div class="card-text">
<div class="skeleton-loader"></div>
<div class="skeleton-loader"></div>

View File

@@ -1,7 +1,7 @@
.card-title {
color: #4a68b9;
font-size: 10px;
margin-bottom: 4px;
margin-bottom: 4px;
font-size: 1rem;
}
@@ -21,28 +21,17 @@
.fee-estimation-container {
display: flex;
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
flex-direction: row;
.item {
max-width: 150px;
width: 100px;
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;
}
&:first-child {
@media (767px < width < 992px), (width < 576px) {
display: none
}
}
margin: 0 auto 0px;
&:last-child {
margin-bottom: 0;
}
@@ -55,18 +44,17 @@
border-bottom: 1px solid #ffffff1c;
width: fit-content;
margin: auto;
line-height: 1.45;
padding: 0px 2px;
font-size: 20px;
}
.fiat {
display: block;
font-size: 14px !important;
font-size: 13px !important;
}
}
}
.loading-container{
min-height: 76px;
height: 50px;
}
.card-text {
@@ -74,12 +62,62 @@
width: 100%;
display: block;
&:first-child {
max-width: 90px;
margin: 15px auto 3px;
max-width: 70px;
margin: 10px auto 3px;
}
&:last-child {
margin: 10px auto 3px;
max-width: 55px;
}
}
}
.fee-progress-bar {
width: 25%;
height: 22px;
margin-bottom: 12px;
display: flex;
flex-direction: row;
transition: background-color 1s;
&.priority {
@media (767px < width < 992px), (width < 576px) {
width: 100%;
}
width: 75%;
border-radius: 0px 10px 10px 0px;
}
&:first-child {
@media (767px < width < 992px), (width < 576px) {
display: none
}
}
}
.band-separator {
width: 5%;
@media (767px < width < 992px), (width < 576px) {
display: none
}
&.fill {
height: 22px;
background: repeating-linear-gradient(
90deg,
rgb(45, 51, 72),
rgb(45, 51, 72) 2px,
rgb(29, 31, 49) 2px,
rgb(29, 31, 49) 4px
);
}
}
.fee-label {
padding-top: 2px;
font-size: 12px;
width: 100%;
@media (767px < width < 992px), (width < 576px) {
width: 33%;
}
&.prority {
width: 33%;
}
}

View File

@@ -1,14 +1,9 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { StateService } from 'src/app/services/state.service';
import { map, filter } from 'rxjs/operators';
import { merge, Observable } from 'rxjs';
import { MempoolBlock } from 'src/app/interfaces/websocket.interface';
interface FeeEstimations {
fastestFee: number;
halfHourFee: number;
hourFee: number;
}
import { Observable } from 'rxjs';
import { Recommendedfees } from 'src/app/interfaces/websocket.interface';
import { feeLevels, mempoolFeeColors } from 'src/app/app.constants';
import { tap } from 'rxjs/operators';
@Component({
selector: 'app-fees-box',
@@ -17,52 +12,32 @@ interface FeeEstimations {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FeesBoxComponent implements OnInit {
feeEstimations$: Observable<FeeEstimations>;
isLoadingWebSocket$: Observable<boolean>;
defaultFee: number;
recommendedFees$: Observable<Recommendedfees>;
gradient = 'linear-gradient(to right, #2e324e, #2e324e)';
noPriority = '#2e324e';
constructor(
private stateService: StateService,
private stateService: StateService
) { }
ngOnInit(): void {
this.defaultFee = this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ? 0.1 : 1;
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.feeEstimations$ = this.stateService.mempoolBlocks$
this.recommendedFees$ = this.stateService.recommendedFees$
.pipe(
map((pBlocks) => {
if (!pBlocks.length) {
return {
'fastestFee': this.defaultFee,
'halfHourFee': this.defaultFee,
'hourFee': this.defaultFee,
};
}
tap((fees) => {
let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.minimumFee >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
const startColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
const firstMedianFee = this.optimizeMedianFee(pBlocks[0], pBlocks[1]);
const secondMedianFee = pBlocks[1] ? this.optimizeMedianFee(pBlocks[1], pBlocks[2], firstMedianFee) : this.defaultFee;
const thirdMedianFee = pBlocks[2] ? this.optimizeMedianFee(pBlocks[2], pBlocks[3], secondMedianFee) : this.defaultFee;
feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => fees.fastestFee >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
const endColor = '#' + (mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
return {
'fastestFee': firstMedianFee,
'halfHourFee': secondMedianFee,
'hourFee': thirdMedianFee,
};
})
);
this.gradient = `linear-gradient(to right, ${startColor}, ${endColor})`;
this.noPriority = startColor;
}
)
);
}
private optimizeMedianFee(pBlock: MempoolBlock, nextBlock: MempoolBlock | undefined, previousFee?: number): number {
const useFee = previousFee ? (pBlock.medianFee + previousFee) / 2 : pBlock.medianFee;
if (pBlock.blockVSize <= 500000) {
return this.defaultFee;
}
if (pBlock.blockVSize <= 950000 && !nextBlock) {
const multiplier = (pBlock.blockVSize - 500000) / 500000;
return Math.max(Math.round(useFee * multiplier), this.defaultFee);
}
return Math.ceil(useFee);
}
}

View File

@@ -3,7 +3,8 @@
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
<img [src]="officialMempoolSpace ? './resources/mempool-space-logo.png' : './resources/mempool-logo.png'" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo">
<img *ngIf="!officialMempoolSpace" src="./resources/mempool-logo.png" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo">
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 140px; height: 35px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
<div class="connection-badge">
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>

View File

@@ -0,0 +1,682 @@
import { FastVertexArray } from './fast-vertex-array'
import TxView from './tx-view'
import { TransactionStripped } from 'src/app/interfaces/websocket.interface';
import { Position, Square } from './sprite-types'
export default class BlockScene {
scene: { count: number, offset: { x: number, y: number}};
vertexArray: FastVertexArray;
txs: { [key: string]: TxView };
width: number;
height: number;
gridWidth: number;
gridHeight: number;
gridSize: number;
vbytesPerUnit: number;
unitPadding: number;
unitWidth: number;
initialised: boolean;
layout: BlockLayout;
dirty: boolean;
constructor ({ width, height, resolution, blockLimit, vertexArray }: { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }) {
this.init({ width, height, resolution, blockLimit, vertexArray })
}
destroy (): void {
Object.values(this.txs).forEach(tx => tx.destroy())
}
resize ({ width = this.width, height = this.height }: { width?: number, height?: number}): void {
this.width = width
this.height = height
this.gridSize = this.width / this.gridWidth
this.unitPadding = Math.floor(Math.max(1, width / 1000))
this.unitWidth = this.gridSize - (this.unitPadding * 2)
this.dirty = true
if (this.initialised && this.scene) this.updateAll(performance.now())
}
// Animate new block entering scene
enter (txs: TransactionStripped[], direction) {
this.replace(txs, direction)
}
// Animate block leaving scene
exit (direction: string): void {
const startTime = performance.now()
const removed = this.removeBatch(Object.keys(this.txs), startTime, direction)
// clean up sprites
setTimeout(() => {
removed.forEach(tx => {
tx.destroy()
})
}, 2000)
}
// Reset layout and replace with new set of transactions
replace (txs: TransactionStripped[], direction: string = 'left'): void {
const startTime = performance.now()
const nextIds = {}
const remove = []
txs.forEach(tx => {
nextIds[tx.txid] = true
})
Object.keys(this.txs).forEach(txid => {
if (!nextIds[txid]) remove.push(txid)
})
txs.forEach(tx => {
if (!this.txs[tx.txid]) this.txs[tx.txid] = new TxView(tx, this.vertexArray)
})
const removed = this.removeBatch(remove, startTime, direction)
// clean up sprites
setTimeout(() => {
removed.forEach(tx => {
tx.destroy()
})
}, 1000)
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight })
Object.values(this.txs).sort((a,b) => { return b.feerate - a.feerate }).forEach(tx => {
this.place(tx)
})
this.updateAll(startTime, direction)
}
update (add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void {
const startTime = performance.now()
const removed = this.removeBatch(remove, startTime, direction)
// clean up sprites
setTimeout(() => {
removed.forEach(tx => {
tx.destroy()
})
}, 1000)
if (resetLayout) {
add.forEach(tx => {
if (!this.txs[tx.txid]) this.txs[tx.txid] = new TxView(tx, this.vertexArray)
})
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight })
Object.values(this.txs).sort((a,b) => { return b.feerate - a.feerate }).forEach(tx => {
this.place(tx)
})
} else {
// try to insert new txs directly
const remaining = []
add.map(tx => new TxView(tx, this.vertexArray)).sort((a,b) => { return b.feerate - a.feerate }).forEach(tx => {
if (!this.tryInsertByFee(tx)) {
remaining.push(tx)
}
})
this.placeBatch(remaining)
this.layout.applyGravity()
}
this.updateAll(startTime, direction)
}
//return the tx at this screen position, if any
getTxAt (position: Position): TxView | void {
if (this.layout) {
const gridPosition = this.screenToGrid(position)
return this.layout.getTx(gridPosition)
} else return null
}
private init ({ width, height, resolution, blockLimit, vertexArray }: { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }): void {
this.vertexArray = vertexArray
this.scene = {
count: 0,
offset: {
x: 0,
y: 0
}
}
// Set the scale of the visualization (with a 5% margin)
this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.05, 2)
this.gridWidth = resolution
this.gridHeight = resolution
this.resize({ width, height })
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight })
this.txs = {}
this.initialised = true
this.dirty = true
}
private insert (tx: TxView, startTime: number, direction: string = 'left'): void {
this.txs[tx.txid] = tx
this.place(tx)
this.updateTx(tx, startTime, direction)
}
private updateTx (tx: TxView, startTime: number, direction: string = 'left'): void {
if (tx.dirty || this.dirty) {
this.saveGridToScreenPosition(tx)
this.setTxOnScreen(tx, startTime, direction)
}
}
private setTxOnScreen (tx: TxView, startTime: number, direction: string = 'left'): void {
if (!tx.initialised) {
const txColor = tx.getColor()
tx.update({
display: {
position: {
x: tx.screenPosition.x + (direction == 'right' ? -this.width : this.width) * 1.4,
y: tx.screenPosition.y,
s: tx.screenPosition.s
},
color: txColor,
},
start: startTime,
delay: 0,
})
tx.update({
display: {
position: tx.screenPosition,
color: txColor
},
duration: 1000,
start: startTime,
delay: 50,
})
} else {
tx.update({
display: {
position: tx.screenPosition
},
duration: 1000,
minDuration: 500,
start: startTime,
delay: 50,
adjust: true
})
}
}
private updateAll (startTime: number, direction: string = 'left'): void {
this.scene.count = 0
const ids = this.getTxList()
startTime = startTime || performance.now()
for (let i = 0; i < ids.length; i++) {
this.updateTx(this.txs[ids[i]], startTime, direction)
}
this.dirty = false
}
private remove (id: string, startTime: number, direction: string = 'left'): TxView | void {
const tx = this.txs[id]
if (tx) {
this.layout.remove(tx)
tx.update({
display: {
position: {
x: tx.screenPosition.x + (direction == 'right' ? this.width : -this.width) * 1.4,
y: this.txs[id].screenPosition.y,
}
},
duration: 1000,
start: startTime,
delay: 50
})
}
delete this.txs[id]
return tx
}
private getTxList (): string[] {
return Object.keys(this.txs)
}
private saveGridToScreenPosition (tx: TxView): void {
tx.screenPosition = this.gridToScreen(tx.gridPosition)
}
// convert grid coordinates to screen coordinates
private gridToScreen (position: Square | void): Square {
if (position) {
const slotSize = (position.s * this.gridSize)
const squareSize = slotSize - (this.unitPadding * 2)
// The grid is laid out notionally left-to-right, bottom-to-top
// So we rotate 90deg counterclockwise then flip the y axis
//
// grid screen
// ________ ________ ________
// | | | b| | a|
// | | rotate | | flip | c |
// | c | --> | c | --> | |
// |a______b| |_______a| |_______b|
return {
x: this.width + (this.unitPadding * 2) - (this.gridSize * position.y) - slotSize,
y: this.height - ((this.gridSize * position.x) + (slotSize - this.unitPadding)),
s: squareSize
}
} else {
return { x: 0, y: 0, s: 0 }
}
}
screenToGrid (position: Position): Position {
const grid = {
x: Math.floor((position.y - this.unitPadding) / this.gridSize),
y: Math.floor((this.width + (this.unitPadding * 2) - position.x) / this.gridSize)
}
return grid
}
// calculates and returns the size of the tx in multiples of the grid size
private txSize (tx: TxView): number {
let scale = Math.max(1,Math.round(Math.sqrt(tx.vsize / this.vbytesPerUnit)))
return Math.min(this.gridWidth, Math.max(1, scale)) // bound between 1 and the max displayable size (just in case!)
}
private place (tx: TxView): void {
const size = this.txSize(tx)
this.layout.insert(tx, size)
}
private tryInsertByFee (tx: TxView): boolean {
const size = this.txSize(tx)
const position = this.layout.tryInsertByFee(tx, size)
if (position) {
this.txs[tx.txid] = tx
return true
} else {
return false
}
}
// Add a list of transactions to the layout,
// keeping everything approximately sorted by feerate.
private placeBatch (txs: TxView[]): void {
if (txs.length) {
// grab the new tx with the highest fee rate
txs = txs.sort((a,b) => { return b.feerate - a.feerate })
let i = 0
let maxSize = txs.reduce((max, tx) => {
return Math.max(this.txSize(tx), max)
}, 1) * 2
// find a reasonable place for it in the layout
const root = this.layout.getReplacementRoot(txs[0].feerate, maxSize)
// extract a sub tree of transactions from the layout, rooted at that point
const popped = this.layout.popTree(root.x, root.y, maxSize)
// combine those with the new transactions and sort
txs = txs.concat(popped)
txs = txs.sort((a,b) => { return b.feerate - a.feerate })
// insert everything back into the layout
txs.forEach(tx => {
this.txs[tx.txid] = tx
this.place(tx)
})
}
}
private removeBatch (ids: string[], startTime: number, direction: string = 'left'): TxView[] {
if (!startTime) startTime = performance.now()
return ids.map(id => {
return this.remove(id, startTime, direction)
}).filter(tx => tx != null) as TxView[]
}
}
class Slot {
l: number
r: number
w: number
constructor (l: number, r: number) {
this.l = l
this.r = r
this.w = r - l
}
intersects (slot: Slot): boolean {
return !((slot.r <= this.l) || (slot.l >= this.r))
}
subtract (slot: Slot): Slot[] | void {
if (this.intersects(slot)) {
// from middle
if (slot.l > this.l && slot.r < this.r) {
return [
new Slot(this.l, slot.l),
new Slot(slot.r, this.r)
]
} // totally covered
else if (slot.l <= this.l && slot.r >= this.r) {
return []
} // from left side
else if (slot.l <= this.l) {
if (slot.r == this.r) return []
else return [new Slot(slot.r, this.r)]
} // from right side
else if (slot.r >= this.r) {
if (slot.l == this.l) return []
else return [new Slot(this.l, slot.l)]
}
} else return [this]
}
}
class TxSlot extends Slot {
tx: TxView
constructor (l: number, r: number, tx: TxView) {
super(l, r)
this.tx = tx
}
}
class Row {
y: number
w: number
filled: TxSlot[]
slots: Slot[]
constructor (y: number, width: number) {
this.y = y
this.w = width
this.filled = []
this.slots = [new Slot(0, this.w)]
}
// insert a transaction w/ given width into row starting at position x
insert (x: number, w: number, tx: TxView): void {
const newSlot = new TxSlot(x, x + w, tx)
// insert into filled list
let index = this.filled.findIndex((slot) => { return slot.l >= newSlot.r })
if (index < 0) index = this.filled.length
this.filled.splice(index || 0, 0, newSlot)
// subtract from overlapping slots
for (let i = 0; i < this.slots.length; i++) {
if (newSlot.intersects(this.slots[i])) {
const diff = this.slots[i].subtract(newSlot)
if (diff) {
this.slots.splice(i, 1, ...diff)
i += diff.length - 1
}
}
}
}
remove (x: number, w: number): void {
const txIndex = this.filled.findIndex((slot) => { return slot.l == x })
this.filled.splice(txIndex, 1)
const newSlot = new Slot(x, x + w)
let slotIndex = this.slots.findIndex((slot) => { return slot.l >= newSlot.r })
if (slotIndex < 0) slotIndex = this.slots.length
this.slots.splice(slotIndex || 0, 0, newSlot)
this.normalize()
}
// merge any contiguous empty slots
private normalize (): void {
for (let i = 0; i < this.slots.length - 1; i++) {
if (this.slots[i].r == this.slots[i+1].l) {
this.slots[i].r = this.slots[i+1].r
this.slots[i].w += this.slots[i+1].w
this.slots.splice(i+1, 1)
i--
}
}
}
txAt (x: number): TxView | void {
let i = 0
while (i < this.filled.length && this.filled[i].l <= x) {
if (this.filled[i].l <= x && this.filled[i].r > x) return this.filled[i].tx
i++
}
}
getSlotsBetween (left: number, right: number): TxSlot[] {
const range = new Slot(left, right)
return this.filled.filter(slot => {
return slot.intersects(range)
})
}
slotAt (x: number): Slot | void {
let i = 0
while (i < this.slots.length && this.slots[i].l <= x) {
if (this.slots[i].l <= x && this.slots[i].r > x) return this.slots[i]
i++
}
}
getAvgFeerate (): number {
let count = 0
let total = 0
this.filled.forEach(slot => {
if (slot.tx) {
count += slot.w
total += (slot.tx.feerate * slot.w)
}
})
return total / count
}
}
class BlockLayout {
width: number;
height: number;
rows: Row[];
txPositions: { [key: string]: Square }
txs: { [key: string]: TxView }
constructor ({ width, height } : { width: number, height: number }) {
this.width = width
this.height = height
this.rows = [new Row(0, this.width)]
this.txPositions = {}
this.txs = {}
}
getRow (position: Square): Row {
return this.rows[position.y]
}
getTx (position: Square): TxView | void {
if (this.getRow(position)) {
return this.getRow(position).txAt(position.x)
}
}
addRow (): void {
this.rows.push(new Row(this.rows.length, this.width))
}
remove (tx: TxView) {
const position = this.txPositions[tx.txid]
if (position) {
for (let y = position.y; y < position.y + position.s && y < this.rows.length; y++) {
this.rows[y].remove(position.x, position.s)
}
}
delete this.txPositions[tx.txid]
delete this.txs[tx.txid]
}
insert (tx: TxView, width: number): Square {
const fit = this.fit(tx, width)
// insert the tx into rows at that position
for (let y = fit.y; y < fit.y + width; y++) {
if (y >= this.rows.length) this.addRow()
this.rows[y].insert(fit.x, width, tx)
}
const position = { x: fit.x, y: fit.y, s: width }
this.txPositions[tx.txid] = position
this.txs[tx.txid] = tx
tx.applyGridPosition(position)
return position
}
// Find the first slot large enough to hold a transaction of this size
fit (tx: TxView, width: number): Square {
let fit
for (let y = 0; y < this.rows.length && !fit; y++) {
fit = this.findFit(0, this.width, y, y, width)
}
// fall back to placing tx in a new row at the top of the layout
if (!fit) {
fit = { x: 0, y: this.rows.length }
}
return fit
}
// recursively check rows to see if there's space for a tx (depth-first)
// left/right: initial column boundaries to check
// row: current row to check
// start: starting row
// size: size of space needed
findFit (left: number, right: number, row: number, start: number, size: number) : Square {
if ((row - start) >= size || row >= this.rows.length) {
return { x: left, y: start }
}
for (let i = 0; i < this.rows[row].slots.length; i++) {
const slot = this.rows[row].slots[i]
const l = Math.max(left, slot.l)
const r = Math.min(right, slot.r)
if (r - l >= size) {
const fit = this.findFit(l, r, row + 1, start, size)
if (fit) return fit
}
}
}
// insert only if the tx fits into a fee-appropriate position
tryInsertByFee (tx: TxView, size: number): Square | void {
const fit = this.fit(tx, size)
if (this.checkRowFees(fit.y, tx.feerate)) {
// insert the tx into rows at that position
for (let y = fit.y; y < fit.y + size; y++) {
if (y >= this.rows.length) this.addRow()
this.rows[y].insert(fit.x, size, tx)
}
const position = { x: fit.x, y: fit.y, s: size }
this.txPositions[tx.txid] = position
this.txs[tx.txid] = tx
tx.applyGridPosition(position)
return position
}
}
// Return the first slot with a lower feerate
getReplacementRoot (feerate: number, width: number): Square {
let slot
for (let row = 0; row <= this.rows.length; row++) {
if (this.rows[row].slots.length > 0) {
return { x: this.rows[row].slots[0].l, y: row }
} else {
slot = this.rows[row].filled.find(x => {
return x.tx.feerate < feerate
})
if (slot) {
return { x: Math.min(slot.l, this.width - width), y: row }
}
}
}
return { x: 0, y: this.rows.length }
}
// remove and return all transactions in a subtree of the layout
popTree (x: number, y: number, width: number) {
const selected: { [key: string]: TxView } = {}
let left = x
let right = x + width
let prevWidth = right - left
let prevFee = Infinity
// scan rows upwards within a channel bounded by 'left' and 'right'
for (let row = y; row < this.rows.length; row++) {
let rowMax = 0
let slots = this.rows[row].getSlotsBetween(left, right)
// check each slot in this row overlapping the search channel
slots.forEach(slot => {
// select the associated transaction
selected[slot.tx.txid] = slot.tx
rowMax = Math.max(rowMax, slot.tx.feerate)
// widen the search channel to accommodate this slot if necessary
if (slot.w > prevWidth) {
left = slot.l
right = slot.r
// if this slot's tx has a higher feerate than the max in the previous row
// (i.e. it's out of position)
// select all txs overlapping the slot's full width in some rows *below*
// to free up space for this tx to sink down to its proper position
if (slot.tx.feerate > prevFee) {
let count = 0
// keep scanning back down until we find a full row of higher-feerate txs
for (let echo = row - 1; echo >= 0 && count < slot.w; echo--) {
let echoSlots = this.rows[echo].getSlotsBetween(slot.l, slot.r)
count = 0
echoSlots.forEach(echoSlot => {
selected[echoSlot.tx.txid] = echoSlot.tx
if (echoSlot.tx.feerate >= slot.tx.feerate) {
count += echoSlot.w
}
})
}
}
}
})
prevWidth = right - left
prevFee = rowMax
}
const txList = Object.values(selected)
txList.forEach(tx => {
this.remove(tx)
})
return txList
}
// Check if this row has high enough avg fees
// for a tx with this feerate to make sense here
checkRowFees (row: number, targetFee: number): boolean {
// first row is always fine
if (row == 0 || !this.rows[row]) return true
return (this.rows[row].getAvgFeerate() > (targetFee * 0.9))
}
// drop any free-floating transactions down into empty spaces
applyGravity (): void {
Object.entries(this.txPositions).sort(([keyA, posA], [keyB, posB]) => {
return posA.y - posB.y || posA.x - posB.x
}).forEach(([txid, position]) => {
// see how far this transaction can fall
let dropTo = position.y
while (dropTo > 0 && !this.rows[dropTo - 1].getSlotsBetween(position.x, position.x + position.s).length) {
dropTo--;
}
// if it can fall at all
if (dropTo < position.y) {
// remove and reinsert in the row we found
const tx = this.txs[txid]
this.remove(tx)
this.insert(tx, position.s)
}
})
}
}

View File

@@ -0,0 +1,105 @@
/*
Utility class for access and management of low-level sprite data
Maintains a single Float32Array of sprite data, keeping track of empty slots
to allow constant-time insertion and deletion
Automatically resizes by copying to a new, larger Float32Array when necessary,
or compacting into a smaller Float32Array when there's space to do so.
*/
import TxSprite from './tx-sprite';
export class FastVertexArray {
length: number;
count: number;
stride: number;
sprites: TxSprite[];
data: Float32Array;
freeSlots: number[];
lastSlot: number;
constructor(length, stride) {
this.length = length;
this.count = 0;
this.stride = stride;
this.sprites = [];
this.data = new Float32Array(this.length * this.stride);
this.freeSlots = [];
this.lastSlot = 0;
}
insert(sprite: TxSprite): number {
this.count++;
let position;
if (this.freeSlots.length) {
position = this.freeSlots.shift();
} else {
position = this.lastSlot;
this.lastSlot++;
if (this.lastSlot > this.length) {
this.expand();
}
}
this.sprites[position] = sprite;
return position;
}
remove(index: number): void {
this.count--;
this.clearData(index);
this.freeSlots.push(index);
this.sprites[index] = null;
if (this.length > 2048 && this.count < (this.length * 0.4)) {
this.compact();
}
}
setData(index: number, dataChunk: number[]): void {
this.data.set(dataChunk, (index * this.stride));
}
clearData(index: number): void {
this.data.fill(0, (index * this.stride), ((index + 1) * this.stride));
}
getData(index: number): Float32Array {
return this.data.subarray(index, this.stride);
}
expand(): void {
this.length *= 2;
const newData = new Float32Array(this.length * this.stride);
newData.set(this.data);
this.data = newData;
}
compact(): void {
// New array length is the smallest power of 2 larger than the sprite count (but no smaller than 512)
const newLength = Math.max(512, Math.pow(2, Math.ceil(Math.log2(this.count))));
if (newLength !== this.length) {
this.length = newLength;
this.data = new Float32Array(this.length * this.stride);
let sprite;
const newSprites = [];
let i = 0;
for (const index in this.sprites) {
sprite = this.sprites[index];
if (sprite) {
newSprites.push(sprite);
sprite.moveVertexPointer(i);
sprite.compile();
i++;
}
}
this.sprites = newSprites;
this.freeSlots = [];
this.lastSlot = i;
}
}
getVertexData(): Float32Array {
return this.data;
}
}

View File

@@ -0,0 +1,6 @@
<div class="mempool-block-overview">
<canvas class="block-overview" #blockCanvas></canvas>
<div class="loader-wrapper" [class.hidden]="!(isLoading$ | async)">
<div class="spinner-border ml-3 loading" role="status"></div>
</div>
</div>

View File

@@ -0,0 +1,35 @@
.mempool-block-overview {
position: relative;
width: 100%;
padding-bottom: 100%;
background: #181b2d;
display: flex;
justify-content: center;
align-items: center;
}
.block-overview {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
overflow: hidden;
}
.loader-wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
transition: opacity 500ms 500ms;
&.hidden {
opacity: 0;
transition: opacity 500ms;
}
}

View File

@@ -0,0 +1,402 @@
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, OnInit,
OnDestroy, OnChanges, ChangeDetectionStrategy, NgZone, AfterViewInit } from '@angular/core';
import { StateService } from 'src/app/services/state.service';
import { MempoolBlockWithTransactions, MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface';
import { Subscription, BehaviorSubject } from 'rxjs';
import { WebsocketService } from 'src/app/services/websocket.service';
import { FastVertexArray } from './fast-vertex-array';
import BlockScene from './block-scene';
import TxSprite from './tx-sprite';
import TxView from './tx-view';
@Component({
selector: 'app-mempool-block-overview',
templateUrl: './mempool-block-overview.component.html',
styleUrls: ['./mempool-block-overview.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
@Input() index: number;
@Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>();
@ViewChild('blockCanvas')
canvas: ElementRef<HTMLCanvasElement>;
gl: WebGLRenderingContext;
animationFrameRequest: number;
displayWidth: number;
displayHeight: number;
shaderProgram: WebGLProgram;
vertexArray: FastVertexArray;
running: boolean;
scene: BlockScene;
hoverTx: TxView | void;
selectedTx: TxView | void;
lastBlockHeight: number;
blockIndex: number;
isLoading$ = new BehaviorSubject<boolean>(true);
blockSub: Subscription;
deltaSub: Subscription;
constructor(
public stateService: StateService,
private websocketService: WebsocketService,
readonly ngZone: NgZone,
) {
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
}
ngOnInit(): void {
this.blockSub = this.stateService.mempoolBlockTransactions$.subscribe((transactionsStripped) => {
this.replaceBlock(transactionsStripped);
});
this.deltaSub = this.stateService.mempoolBlockDelta$.subscribe((delta) => {
this.updateBlock(delta);
});
}
ngAfterViewInit(): void {
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
this.gl = this.canvas.nativeElement.getContext('webgl');
this.initCanvas();
this.resizeCanvas();
}
ngOnChanges(changes): void {
if (changes.index) {
this.clearBlock(changes.index.currentValue > changes.index.previousValue ? 'right' : 'left');
this.isLoading$.next(true);
this.websocketService.startTrackMempoolBlock(changes.index.currentValue);
}
}
ngOnDestroy(): void {
this.blockSub.unsubscribe();
this.deltaSub.unsubscribe();
this.websocketService.stopTrackMempoolBlock();
}
clearBlock(direction): void {
if (this.scene) {
this.scene.exit(direction);
}
this.hoverTx = null;
this.selectedTx = null;
this.txPreviewEvent.emit(null);
}
replaceBlock(transactionsStripped: TransactionStripped[]): void {
if (!this.scene) {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray });
}
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
if (this.blockIndex !== this.index) {
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right';
this.scene.enter(transactionsStripped, direction);
} else {
this.scene.replace(transactionsStripped, blockMined ? 'right' : 'left');
}
this.lastBlockHeight = this.stateService.latestBlockHeight;
this.blockIndex = this.index;
this.isLoading$.next(false);
}
updateBlock(delta: MempoolBlockDelta): void {
if (!this.scene) {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray });
}
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
if (this.blockIndex !== this.index) {
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right';
this.scene.exit(direction);
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray });
this.scene.enter(delta.added, direction);
} else {
this.scene.update(delta.added, delta.removed, blockMined ? 'right' : 'left', blockMined);
}
this.lastBlockHeight = this.stateService.latestBlockHeight;
this.blockIndex = this.index;
this.isLoading$.next(false);
}
initCanvas(): void {
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
const shaderSet = [
{
type: this.gl.VERTEX_SHADER,
src: vertShaderSrc
},
{
type: this.gl.FRAGMENT_SHADER,
src: fragShaderSrc
}
];
this.shaderProgram = this.buildShaderProgram(shaderSet);
this.gl.useProgram(this.shaderProgram);
// Set up alpha blending
this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
const glBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, glBuffer);
/* SET UP SHADER ATTRIBUTES */
Object.keys(attribs).forEach((key, i) => {
attribs[key].pointer = this.gl.getAttribLocation(this.shaderProgram, key);
this.gl.enableVertexAttribArray(attribs[key].pointer);
});
this.start();
}
handleContextLost(event): void {
event.preventDefault();
cancelAnimationFrame(this.animationFrameRequest);
this.animationFrameRequest = null;
this.running = false;
}
handleContextRestored(event): void {
this.initCanvas();
}
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
this.displayWidth = this.canvas.nativeElement.parentElement.clientWidth;
this.displayHeight = this.canvas.nativeElement.parentElement.clientHeight;
this.canvas.nativeElement.width = this.displayWidth;
this.canvas.nativeElement.height = this.displayHeight;
if (this.gl) {
this.gl.viewport(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
}
if (this.scene) {
this.scene.resize({ width: this.displayWidth, height: this.displayHeight });
}
}
compileShader(src, type): WebGLShader {
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, src);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
console.log(`Error compiling ${type === this.gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:`);
console.log(this.gl.getShaderInfoLog(shader));
}
return shader;
}
buildShaderProgram(shaderInfo): WebGLProgram {
const program = this.gl.createProgram();
shaderInfo.forEach((desc) => {
const shader = this.compileShader(desc.src, desc.type);
if (shader) {
this.gl.attachShader(program, shader);
}
});
this.gl.linkProgram(program);
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
console.log('Error linking shader program:');
console.log(this.gl.getProgramInfoLog(program));
}
return program;
}
start(): void {
this.running = true;
this.ngZone.runOutsideAngular(() => this.run());
}
run(now?: DOMHighResTimeStamp): void {
if (!now) {
now = performance.now();
}
/* SET UP SHADER UNIFORMS */
// screen dimensions
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
// frame timestamp
this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, 'now'), now);
/* SET UP SHADER ATTRIBUTES */
Object.keys(attribs).forEach((key, i) => {
this.gl.vertexAttribPointer(attribs[key].pointer,
attribs[key].count, // number of primitives in this attribute
this.gl[attribs[key].type], // type of primitive in this attribute (e.g. gl.FLOAT)
false, // never normalised
stride, // distance between values of the same attribute
attribs[key].offset); // offset of the first value
});
const pointArray = this.vertexArray.getVertexData();
if (pointArray.length) {
this.gl.bufferData(this.gl.ARRAY_BUFFER, pointArray, this.gl.DYNAMIC_DRAW);
this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
}
/* LOOP */
if (this.running) {
if (this.animationFrameRequest) {
cancelAnimationFrame(this.animationFrameRequest);
this.animationFrameRequest = null;
}
this.animationFrameRequest = requestAnimationFrame(() => this.run());
}
}
@HostListener('click', ['$event'])
onClick(event) {
this.setPreviewTx(event.offsetX, event.offsetY, true);
}
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
this.setPreviewTx(event.offsetX, event.offsetY, false);
}
@HostListener('pointerleave', ['$event'])
onPointerLeave(event) {
this.setPreviewTx(-1, -1, false);
}
setPreviewTx(x: number, y: number, clicked: boolean = false) {
if (this.scene && (!this.selectedTx || clicked)) {
const selected = this.scene.getTxAt({ x, y });
const currentPreview = this.selectedTx || this.hoverTx;
if (selected !== currentPreview) {
if (currentPreview) {
currentPreview.setHover(false);
}
if (selected) {
selected.setHover(true);
this.txPreviewEvent.emit({
txid: selected.txid,
fee: selected.fee,
vsize: selected.vsize,
value: selected.value
});
if (clicked) {
this.selectedTx = selected;
} else {
this.hoverTx = selected;
}
} else {
if (clicked) {
this.selectedTx = null;
}
this.hoverTx = null;
this.txPreviewEvent.emit(null);
}
} else if (clicked) {
if (selected === this.selectedTx) {
this.hoverTx = this.selectedTx;
this.selectedTx = null;
} else {
this.selectedTx = selected;
}
}
}
}
}
// WebGL shader attributes
const attribs = {
offset: { type: 'FLOAT', count: 2, pointer: null, offset: 0 },
posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
posY: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
posR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colG: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
colA: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }
};
// Calculate the number of bytes per vertex based on specified attributes
const stride = Object.values(attribs).reduce((total, attrib) => {
return total + (attrib.count * 4);
}, 0);
// Calculate vertex attribute offsets
for (let i = 0, offset = 0; i < Object.keys(attribs).length; i++) {
const attrib = Object.values(attribs)[i];
attrib.offset = offset;
offset += (attrib.count * 4);
}
const vertShaderSrc = `
varying lowp vec4 vColor;
// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
// shader interpolates between start and end values at the given rate, from the given time
attribute vec2 offset;
attribute vec4 posX;
attribute vec4 posY;
attribute vec4 posR;
attribute vec4 colR;
attribute vec4 colG;
attribute vec4 colB;
attribute vec4 colA;
uniform vec2 screenSize;
uniform float now;
float smootherstep(float x) {
x = clamp(x, 0.0, 1.0);
float ix = 1.0 - x;
x = x * x;
return x / (x + ix * ix);
}
float interpolateAttribute(vec4 attr) {
float d = (now - attr.z) * attr.w;
float delta = smootherstep(d);
return mix(attr.x, attr.y, delta);
}
void main() {
vec4 screenTransform = vec4(2.0 / screenSize.x, 2.0 / screenSize.y, -1.0, -1.0);
// vec4 screenTransform = vec4(1.0 / screenSize.x, 1.0 / screenSize.y, -0.5, -0.5);
float radius = interpolateAttribute(posR);
vec2 position = vec2(interpolateAttribute(posX), interpolateAttribute(posY)) + (radius * offset);
gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
float red = interpolateAttribute(colR);
float green = interpolateAttribute(colG);
float blue = interpolateAttribute(colB);
float alpha = interpolateAttribute(colA);
vColor = vec4(red, green, blue, alpha);
}
`;
const fragShaderSrc = `
varying lowp vec4 vColor;
void main() {
gl_FragColor = vColor;
// premultiply alpha
gl_FragColor.rgb *= gl_FragColor.a;
}
`;

View File

@@ -0,0 +1,74 @@
export type Position = {
x: number,
y: number,
};
export type Square = Position & {
s?: number
};
export type Color = {
r: number,
g: number,
b: number,
a: number,
};
export type InterpolatedAttribute = {
a: number,
b: number,
t: number,
v: number,
d: number
};
export type Update = Position & { s: number } & Color;
export type Attributes = {
x: InterpolatedAttribute,
y: InterpolatedAttribute,
s: InterpolatedAttribute,
r: InterpolatedAttribute,
g: InterpolatedAttribute,
b: InterpolatedAttribute,
a: InterpolatedAttribute
};
export type OptionalAttributes = {
x?: InterpolatedAttribute,
y?: InterpolatedAttribute,
s?: InterpolatedAttribute,
r?: InterpolatedAttribute,
g?: InterpolatedAttribute,
b?: InterpolatedAttribute,
a?: InterpolatedAttribute
};
export type SpriteUpdateParams = {
x?: number,
y?: number,
s?: number,
r?: number,
g?: number,
b?: number,
a?: number
start?: DOMHighResTimeStamp,
duration?: number,
minDuration?: number,
adjust?: boolean,
temp?: boolean
};
export type ViewUpdateParams = {
display: {
position?: Square,
color?: Color,
},
start?: number,
duration?: number,
minDuration?: number,
delay?: number,
jitter?: number,
state?: string,
adjust?: boolean
};

View File

@@ -0,0 +1,215 @@
import { FastVertexArray } from './fast-vertex-array';
import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from './sprite-types';
const attribKeys = ['a', 'b', 't', 'v'];
const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];
export default class TxSprite {
static vertexSize = 30;
static vertexCount = 6;
static dataSize: number = (30 * 6);
vertexArray: FastVertexArray;
vertexPointer: number;
vertexData: number[];
updateMap: Update;
attributes: Attributes;
tempAttributes: OptionalAttributes;
constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray) {
const offsetTime = params.start;
this.vertexArray = vertexArray;
this.vertexData = Array(VI.length).fill(0);
this.updateMap = {
x: 0, y: 0, s: 0, r: 0, g: 0, b: 0, a: 0
};
this.attributes = {
x: { a: params.x, b: params.x, t: offsetTime, v: 0, d: 0 },
y: { a: params.y, b: params.y, t: offsetTime, v: 0, d: 0 },
s: { a: params.s, b: params.s, t: offsetTime, v: 0, d: 0 },
r: { a: params.r, b: params.r, t: offsetTime, v: 0, d: 0 },
g: { a: params.g, b: params.g, t: offsetTime, v: 0, d: 0 },
b: { a: params.b, b: params.b, t: offsetTime, v: 0, d: 0 },
a: { a: params.a, b: params.a, t: offsetTime, v: 0, d: 0 },
};
// Used to temporarily modify the sprite, so that the base view can be resumed later
this.tempAttributes = null;
this.vertexPointer = this.vertexArray.insert(this);
this.compile();
}
private interpolateAttributes(updateMap: Update, attributes: OptionalAttributes, offsetTime: DOMHighResTimeStamp, v: number,
duration: number, minDuration: number, adjust: boolean): void {
for (const key of Object.keys(updateMap)) {
// for each non-null attribute:
if (updateMap[key] != null) {
// calculate current interpolated value, and set as 'from'
interpolateAttributeStart(attributes[key], offsetTime);
// update start time
attributes[key].t = offsetTime;
if (!adjust || (duration && attributes[key].d === 0)) {
attributes[key].v = v;
attributes[key].d = duration;
} else if (minDuration > attributes[key].d) {
// enforce minimum transition duration
attributes[key].v = 1 / minDuration;
attributes[key].d = minDuration;
}
// set 'to' to target value
attributes[key].b = updateMap[key];
}
}
}
/*
params:
x, y, s: position & size of the sprite
r, g, b, a: color & opacity
start: performance.now() timestamp, when to start the transition
duration: of the tweening animation
adjust: if true, alter the target value of any conflicting transitions without changing the duration
minDuration: minimum remaining transition duration when adjust = true
temp: if true, this update is only temporary (can be reversed with 'resume')
*/
update(params: SpriteUpdateParams): void {
const offsetTime = params.start || performance.now();
const v = params.duration > 0 ? (1 / params.duration) : 0;
updateKeys.forEach(key => {
this.updateMap[key] = params[key];
});
const isModified = !!this.tempAttributes;
if (!params.temp) {
this.interpolateAttributes(this.updateMap, this.attributes, offsetTime, v, params.duration, params.minDuration, params.adjust);
} else {
if (!isModified) { // set up tempAttributes
this.tempAttributes = {};
for (const key of Object.keys(this.updateMap)) {
if (this.updateMap[key] != null) {
this.tempAttributes[key] = { ...this.attributes[key] };
}
}
}
this.interpolateAttributes(this.updateMap, this.tempAttributes, offsetTime, v, params.duration, params.minDuration, params.adjust);
}
this.compile();
}
// Transition back from modified state back to base attributes
resume(duration: number, start: DOMHighResTimeStamp = performance.now()): void {
// If not in modified state, there's nothing to do
if (!this.tempAttributes) {
return;
}
const offsetTime = start;
const v = duration > 0 ? (1 / duration) : 0;
for (const key of Object.keys(this.tempAttributes)) {
// If this base attribute is static (fixed or post-transition), transition smoothly back
if (this.attributes[key].v === 0 || (this.attributes[key].t + this.attributes[key].d) <= start) {
// calculate current interpolated value, and set as 'from'
interpolateAttributeStart(this.tempAttributes[key], offsetTime);
this.attributes[key].a = this.tempAttributes[key].a;
this.attributes[key].t = offsetTime;
this.attributes[key].v = v;
this.attributes[key].d = duration;
}
}
this.tempAttributes = null;
this.compile();
}
// Write current state into the graphics vertex array for rendering
compile(): void {
let attributes = this.attributes;
if (this.tempAttributes) {
attributes = {
...this.attributes,
...this.tempAttributes
};
}
const size = attributes.s;
// update vertex data in place
// ugly, but avoids overhead of allocating large temporary arrays
const vertexStride = VI.length + 2;
for (let vertex = 0; vertex < 6; vertex++) {
this.vertexData[vertex * vertexStride] = vertexOffsetFactors[vertex][0];
this.vertexData[(vertex * vertexStride) + 1] = vertexOffsetFactors[vertex][1];
for (let step = 0; step < VI.length; step++) {
// components of each field in the vertex array are defined by an entry in VI:
// VI[i].a is the attribute, VI[i].f is the inner field, VI[i].offA and VI[i].offB are offset factors
this.vertexData[(vertex * vertexStride) + step + 2] = attributes[VI[step].a][VI[step].f];
}
}
this.vertexArray.setData(this.vertexPointer, this.vertexData);
}
moveVertexPointer(index: number): void {
this.vertexPointer = index;
}
destroy(): void {
this.vertexArray.remove(this.vertexPointer);
this.vertexPointer = null;
}
}
// expects 0 <= x <= 1
function smootherstep(x: number): number {
const ix = 1 - x;
x = x * x;
return x / (x + ix * ix);
}
function interpolateAttributeStart(attribute: InterpolatedAttribute, start: DOMHighResTimeStamp): void {
if (attribute.v === 0 || (attribute.t + attribute.d) <= start) {
// transition finished, next transition starts from current end state
// (clamp to 1)
attribute.a = attribute.b;
attribute.v = 0;
attribute.d = 0;
} else if (attribute.t > start) {
// transition not started
// (clamp to 0)
} else {
// transition in progress
// (interpolate)
const progress = (start - attribute.t);
const delta = smootherstep(progress / attribute.d);
attribute.a = attribute.a + (delta * (attribute.b - attribute.a));
attribute.d = attribute.d - progress;
attribute.v = 1 / attribute.d;
}
}
const vertexOffsetFactors = [
[0, 0],
[1, 1],
[1, 0],
[0, 0],
[1, 1],
[0, 1]
];
const VI = [];
updateKeys.forEach((attribute, aIndex) => {
attribKeys.forEach(field => {
VI.push({
a: attribute,
f: field
});
});
});

View File

@@ -0,0 +1,150 @@
import TxSprite from './tx-sprite';
import { FastVertexArray } from './fast-vertex-array';
import { TransactionStripped } from 'src/app/interfaces/websocket.interface';
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types';
import { feeLevels, mempoolFeeColors } from 'src/app/app.constants';
const hoverTransitionTime = 300;
const defaultHoverColor = hexToColor('1bd8f4');
// convert from this class's update format to TxSprite's update format
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
return {
start: (params.start || performance.now()) + (params.delay || 0),
duration: params.duration,
minDuration: params.minDuration,
...params.display.position,
...params.display.color,
adjust: params.adjust
};
}
export default class TxView implements TransactionStripped {
txid: string;
fee: number;
vsize: number;
value: number;
feerate: number;
initialised: boolean;
vertexArray: FastVertexArray;
hover: boolean;
sprite: TxSprite;
hoverColor: Color | void;
screenPosition: Square;
gridPosition: Square | void;
dirty: boolean;
constructor(tx: TransactionStripped, vertexArray: FastVertexArray) {
this.txid = tx.txid;
this.fee = tx.fee;
this.vsize = tx.vsize;
this.value = tx.value;
this.feerate = tx.fee / tx.vsize;
this.initialised = false;
this.vertexArray = vertexArray;
this.hover = false;
this.screenPosition = { x: 0, y: 0, s: 0 };
this.dirty = true;
}
destroy(): void {
if (this.sprite) {
this.sprite.destroy();
this.sprite = null;
this.initialised = false;
}
}
applyGridPosition(position: Square): void {
if (!this.gridPosition) {
this.gridPosition = { x: 0, y: 0, s: 0 };
}
if (this.gridPosition.x !== position.x || this.gridPosition.y !== position.y || this.gridPosition.s !== position.s) {
this.gridPosition.x = position.x;
this.gridPosition.y = position.y;
this.gridPosition.s = position.s;
this.dirty = true;
}
}
/*
display: defines the final appearance of the sprite
position: { x, y, s } (coordinates & size)
color: { r, g, b, a} (color channels & alpha)
duration: of the tweening animation from the previous display state
start: performance.now() timestamp, when to start the transition
delay: additional milliseconds to wait before starting
jitter: if set, adds a random amount to the delay,
adjust: if true, modify an in-progress transition instead of replacing it
*/
update(params: ViewUpdateParams): void {
if (params.jitter) {
params.delay += (Math.random() * params.jitter);
}
if (!this.initialised || !this.sprite) {
this.initialised = true;
this.sprite = new TxSprite(
toSpriteUpdate(params),
this.vertexArray
);
// apply any pending hover event
if (this.hover) {
this.sprite.update({
...this.hoverColor,
duration: hoverTransitionTime,
adjust: false,
temp: true
});
}
} else {
this.sprite.update(
toSpriteUpdate(params)
);
}
this.dirty = false;
}
// Temporarily override the tx color
setHover(hoverOn: boolean, color: Color | void = defaultHoverColor): void {
if (hoverOn) {
this.hover = true;
this.hoverColor = color;
this.sprite.update({
...this.hoverColor,
duration: hoverTransitionTime,
adjust: false,
temp: true
});
} else {
this.hover = false;
this.hoverColor = null;
if (this.sprite) {
this.sprite.resume(hoverTransitionTime);
}
}
this.dirty = false;
}
getColor(): Color {
let feeLevelIndex = feeLevels.slice().reverse().findIndex((feeLvl) => (this.feerate || 1) >= feeLvl);
feeLevelIndex = feeLevelIndex >= 0 ? feeLevels.length - feeLevelIndex : feeLevelIndex;
return hexToColor(mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
}
}
function hexToColor(hex: string): Color {
return {
r: parseInt(hex.slice(0, 2), 16) / 255,
g: parseInt(hex.slice(2, 4), 16) / 255,
b: parseInt(hex.slice(4, 6), 16) / 255,
a: 1
};
}

View File

@@ -10,38 +10,69 @@
<div class="box">
<div class="row">
<div class="col-md">
<table class="table table-borderless table-striped">
<table class="table table-borderless table-striped table-fixed">
<tbody>
<tr>
<td i18n="mempool-block.median-fee">Median fee</td>
<td>~{{ mempoolBlock.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="mempoolBlock.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
</tr>
<tr>
<td i18n="mempool-block.fee-span">Fee span</td>
<td><span class="yellow-color">{{ mempoolBlock.feeRange[0] | number:'1.0-0' }} - {{ mempoolBlock.feeRange[mempoolBlock.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
</tr>
<tr>
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
<td><app-amount [satoshis]="mempoolBlock.totalFees" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolBlock.totalFees" digitsInfo="1.0-0"></app-fiat></span></td>
</tr>
<tr>
<td i18n="mempool-block.transactions">Transactions</td>
<td>{{ mempoolBlock.nTx }}</td>
</tr>
<tr>
<td i18n="mempool-block.size">Size</td>
<td>
<div class="progress">
<div class="progress-bar progress-mempool {{ (network$ | async) }}" role="progressbar" [ngStyle]="{'width': (mempoolBlock.blockVSize / stateService.blockVSize) * 100 + '%' }"></div>
<div class="progress-text" [innerHTML]="mempoolBlock.blockSize | bytes: 2"></div>
</div>
</td>
</tr>
<ng-container *ngIf="!previewTx">
<tr>
<td i18n="mempool-block.median-fee">Median fee</td>
<td>~{{ mempoolBlock.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="mempoolBlock.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
</tr>
<tr>
<td i18n="mempool-block.fee-span">Fee span</td>
<td><span class="yellow-color">{{ mempoolBlock.feeRange[0] | number:'1.0-0' }} - {{ mempoolBlock.feeRange[mempoolBlock.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
</tr>
<tr>
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
<td><app-amount [satoshis]="mempoolBlock.totalFees" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> <span class="fiat"><app-fiat [value]="mempoolBlock.totalFees" digitsInfo="1.0-0"></app-fiat></span></td>
</tr>
<tr>
<td i18n="mempool-block.transactions">Transactions</td>
<td>{{ mempoolBlock.nTx }}</td>
</tr>
<tr>
<td i18n="mempool-block.size">Size</td>
<td>
<div class="progress">
<div class="progress-bar progress-mempool {{ (network$ | async) }}" role="progressbar" [ngStyle]="{'width': (mempoolBlock.blockVSize / stateService.blockVSize) * 100 + '%' }"></div>
<div class="progress-text" [innerHTML]="mempoolBlock.blockSize | bytes: 2"></div>
</div>
</td>
</tr>
</ng-container>
<ng-container *ngIf="previewTx">
<tr>
<td i18n="shared.transaction">Transaction</td>
<td>
<a [routerLink]="['/tx/' | relativeUrl, previewTx.txid]">{{ previewTx.txid | shortenString : 16}}</a>
</td>
</tr>
<tr>
<td class="td-width" i18n="transaction.value|Transaction value">Value</td>
<td><app-amount [satoshis]="previewTx.value"></app-amount></td>
</tr>
<tr>
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
<td>{{ previewTx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="previewTx.fee"></app-fiat></span></td>
</tr>
<tr>
<td i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
<td>
{{ (previewTx.fee / previewTx.vsize) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span>
</td>
</tr>
<tr>
<td i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
<td [innerHTML]="'&lrm;' + (previewTx.vsize | vbytes: 2)"></td>
</tr>
</ng-container>
</tbody>
</table>
<app-fee-distribution-graph *ngIf="webGlEnabled" [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph>
</div>
<div class="col-md chart-container">
<app-fee-distribution-graph [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph>
<app-mempool-block-overview *ngIf="webGlEnabled" [index]="mempoolBlockIndex" (txPreviewEvent)="setTxPreview($event)"></app-mempool-block-overview>
<app-fee-distribution-graph *ngIf="!webGlEnabled" [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph>
</div>
</div>
</div>

View File

@@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/
import { StateService } from 'src/app/services/state.service';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { switchMap, map, tap, filter } from 'rxjs/operators';
import { MempoolBlock } from 'src/app/interfaces/websocket.interface';
import { MempoolBlock, TransactionStripped } from 'src/app/interfaces/websocket.interface';
import { Observable, BehaviorSubject } from 'rxjs';
import { SeoService } from 'src/app/services/seo.service';
import { WebsocketService } from 'src/app/services/websocket.service';
@@ -18,13 +18,17 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
mempoolBlockIndex: number;
mempoolBlock$: Observable<MempoolBlock>;
ordinal$: BehaviorSubject<string> = new BehaviorSubject('');
previewTx: TransactionStripped | void;
webGlEnabled: boolean;
constructor(
private route: ActivatedRoute,
public stateService: StateService,
private seoService: SeoService,
private websocketService: WebsocketService,
) { }
) {
this.webGlEnabled = detectWebGL();
}
ngOnInit(): void {
this.websocketService.want(['blocks', 'mempool-blocks']);
@@ -74,5 +78,15 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
} else {
return $localize`:@@mempool-block.block.no:Mempool block ${this.mempoolBlockIndex + 1}:INTERPOLATION:`;
}
}
}
setTxPreview(event: TransactionStripped | void): void {
this.previewTx = event
}
}
function detectWebGL () {
const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
return (gl && gl instanceof WebGLRenderingContext)
}

View File

@@ -238,7 +238,6 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
gradientColors.push(mempoolFeeColors[feeLevelIndex - 1] || mempoolFeeColors[mempoolFeeColors.length - 1]);
});
gradientColors.forEach((color, i, gc) => {
backgroundGradients.push(`
#${i === 0 ? color : gc[i - 1]} ${ i === 0 ? emptyBackgroundSpacePercentage : ((i / gradientColors.length) * 100) * usedBlockSpace / 100 + emptyBackgroundSpacePercentage }%,

View File

@@ -64,7 +64,7 @@
}
.more-padding {
padding: 18px;
padding: 24px 20px !important;
}
.card-wrapper {

View File

@@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, Component, Input, NgZone, OnInit, HostBinding
import { FormBuilder, FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { EChartsOption, PieSeriesOption } from 'echarts';
import { combineLatest, Observable, of } from 'rxjs';
import { catchError, map, share, skip, startWith, switchMap, tap } from 'rxjs/operators';
import { concat, Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { SinglePoolStats } from 'src/app/interfaces/node-api.interface';
import { SeoService } from 'src/app/services/seo.service';
import { StorageService } from '../..//services/storage.service';
@@ -58,15 +58,7 @@ export class PoolRankingComponent implements OnInit {
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
// When...
this.miningStatsObservable$ = combineLatest([
// ...a new block is mined
this.stateService.blocks$
.pipe(
// (we always receives some blocks at start so only trigger for the last one)
skip(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT - 1),
),
// ...or we change the timespan
this.miningStatsObservable$ = concat(
this.radioGroupForm.get('dateSpan').valueChanges
.pipe(
startWith(this.miningWindowPreference), // (trigger when the page loads)
@@ -76,18 +68,19 @@ export class PoolRankingComponent implements OnInit {
this.storageService.setValue('miningWindowPreference', value);
}
this.miningWindowPreference = value;
}),
switchMap(() => {
return this.miningService.getMiningStats(this.miningWindowPreference);
})
)
])
// ...then refresh the mining stats
),
this.stateService.blocks$
.pipe(
switchMap(() => {
return this.miningService.getMiningStats(this.miningWindowPreference);
})
)
)
.pipe(
switchMap(() => {
this.isLoading = true;
return this.miningService.getMiningStats(this.miningWindowPreference)
.pipe(
catchError((e) => of(this.getEmptyMiningStat()))
);
}),
map(data => {
data.pools = data.pools.map((pool: SinglePoolStats) => this.formatPoolUI(pool));
data['minersLuck'] = (100 * (data.blockCount / 1008)).toFixed(2); // luck 1w

View File

@@ -1,6 +1,6 @@
<div class="container-xl">
<img [src]="'./resources/mempool-space-logo.png'" height="35" width="140" class="logo">
<app-svg-images name="officialMempoolSpace" class="logo" style="width: 140px; height: 35px; margin: 30px;" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
<div class="sponsor-page">

View File

@@ -124,10 +124,6 @@
}
}
.logo {
margin: 30px;
}
.card-body {
align-items: center;
display: flex;

View File

@@ -0,0 +1,28 @@
<ng-container [ngSwitch]="name">
<ng-container *ngSwitchCase="'officialMempoolSpace'">
<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="M163.658 113.263C161.089 113.263 158.992 111.146 158.992 108.535C158.992 105.966 161.048 103.951 163.658 103.951C166.269 103.951 168.325 105.966 168.325 108.535C168.325 111.125 166.228 113.263 163.658 113.263Z" fill="#9857FF"/>
<path d="M189.767 113.263C183.867 113.263 177.803 111.392 174.637 108.617L174 108.062L177.104 102.594L178.05 103.334C180.434 105.205 185.08 107.096 190.054 107.096C195.79 107.096 198.565 105.554 198.565 102.368C198.565 99.3461 195.194 98.5856 189.664 97.6194C183.312 96.5094 175.398 95.1115 175.398 86.6012C175.398 79.8999 181.359 75.5625 190.589 75.5625C195.584 75.5625 200.785 76.9192 203.807 79.0365L204.567 79.571L201.484 85.2034L200.559 84.5867C197.804 82.7572 194.269 81.7499 190.609 81.7499C185.368 81.7499 182.366 83.4767 182.366 86.4779C182.366 89.7052 185.84 90.548 191.576 91.5759C197.784 92.6859 205.513 94.0632 205.513 102.224C205.513 105.575 204.012 108.37 201.175 110.323C198.421 112.255 194.474 113.263 189.767 113.263Z" fill="#9857FF"/>
<path d="M212.316 125.863V75.9733H218.997V80.8451C222.122 77.4945 226.541 75.7061 231.722 75.7061C236.984 75.7061 241.774 77.5356 245.186 80.8862C248.66 84.278 250.572 89.0882 250.572 94.4123C250.572 99.7569 248.66 104.588 245.206 108C241.774 111.392 237.005 113.242 231.742 113.242C226.624 113.242 222.327 111.536 219.162 108.247V125.843H212.316V125.863ZM231.434 81.8729C228.001 81.8729 224.876 83.1063 222.656 85.3264C220.354 87.6287 219.141 90.7738 219.141 94.4123C219.141 101.792 224.198 106.952 231.454 106.952C238.69 106.952 243.768 101.792 243.768 94.4123C243.768 90.7738 242.555 87.6287 240.252 85.3264C237.991 83.1063 234.867 81.8729 231.434 81.8729Z" fill="#9857FF"/>
<path d="M269.194 113.262C260.643 113.262 255.113 108.884 255.113 102.1C255.113 98.8936 256.285 96.2212 258.526 94.3712C261.054 92.2744 264.878 91.2055 269.893 91.2055H280.233V90.1777C280.233 84.7714 277.17 82.0168 271.147 82.0168C266.995 82.0168 262.781 83.4558 259.882 85.8814L258.916 86.7037L255.956 81.1329L256.593 80.5985C260.417 77.4328 265.823 75.7061 271.846 75.7061C281.734 75.7061 287.181 80.8862 287.181 90.301V112.975H280.5V109.398C277.89 111.906 273.984 113.262 269.194 113.262ZM270.181 96.6735C262.925 96.6735 262.082 99.613 262.082 101.813C262.082 105.348 264.98 107.363 270.037 107.363H270.058C270.181 107.363 270.304 107.363 270.448 107.363C275.115 107.363 278.547 105.225 280.377 101.175V96.6735H270.181V96.6735Z" fill="#9857FF"/>
<path d="M313.658 112.995C302.742 112.995 294.828 105.184 294.828 94.4123C294.828 89.1088 296.781 84.3191 300.337 80.9274C303.852 77.5561 308.786 75.7061 314.233 75.7061C320.77 75.7061 326.053 78.2961 329.137 83.0241L329.712 83.8875L324.347 87.3204L323.792 86.4776C321.777 83.4558 318.303 81.729 314.254 81.729C310.656 81.729 307.552 82.9213 305.291 85.1825C303.01 87.4643 301.797 90.6094 301.797 94.2684C301.797 97.9685 303.01 101.134 305.332 103.395C307.573 105.595 310.739 106.808 314.233 106.808C318.303 106.808 321.695 105.163 323.771 102.183L324.326 101.381L329.671 104.793L329.137 105.657C326.238 110.323 320.811 112.995 314.254 112.995C314.048 112.995 313.863 112.995 313.658 112.995Z" fill="#9857FF"/>
<path d="M352.118 113.386C346.506 113.386 341.429 111.516 337.852 108.103C334.255 104.691 332.281 99.881 332.281 94.5363C332.281 83.7031 340.031 75.8301 350.7 75.8301C361.204 75.8301 368.83 83.5387 368.995 94.228C368.995 94.8447 368.995 96.9414 368.995 96.9414H339.373C340.36 103.047 345.396 107.076 352.241 107.076C356.414 107.076 360.032 105.596 362.417 102.903L363.177 102.039L367.206 106.603L366.61 107.281C363.218 111.228 358.059 113.386 352.118 113.386ZM362.335 91.3295C361.368 85.6354 356.805 81.7297 350.864 81.7297C344.964 81.7297 340.483 85.5327 339.414 91.3295H362.335Z" fill="#9857FF"/>
<path d="M219.548 56.1979V33.8327C219.548 30.3587 218.746 27.6863 217.163 25.9185C215.601 24.1507 213.237 23.2667 210.195 23.2667C206.762 23.2667 203.946 24.3768 202.013 26.453C200.081 28.5497 199.053 31.6331 199.053 35.395V56.2185H191.447V33.8327C191.447 26.823 188.282 23.2667 182.032 23.2667C178.6 23.2667 175.783 24.3768 173.851 26.453C171.919 28.5497 170.891 31.6331 170.891 35.395V56.2185H163.285V16.4215H170.685V20.7589C173.687 17.7988 178.003 16.1748 182.999 16.1748C188.96 16.1748 193.667 18.4977 196.36 22.7528C199.608 18.5593 204.85 16.1748 210.955 16.1748C215.93 16.1748 219.877 17.6137 222.693 20.43C225.632 23.3901 227.174 27.8713 227.154 33.3599V56.1979H219.548V56.1979Z" fill="white"/>
<path d="M255.397 56.5679C243.145 56.5679 234.594 48.2426 234.594 36.32C234.594 24.644 242.775 16.1748 254.04 16.1748C265.387 16.1748 273.342 24.459 273.383 36.32C273.383 36.4433 273.363 36.5872 273.363 36.7106C273.342 36.8339 273.342 36.9367 273.342 37.0395V37.1011L273.322 37.1628C273.281 37.3684 273.281 37.5945 273.281 37.8617V39.1568H242.343C243.412 45.4881 248.49 49.5377 255.5 49.5377C259.796 49.5377 263.393 47.996 265.901 45.0975L266.888 44.008L271.41 49.1677L270.732 50.0516C267.196 54.2451 261.749 56.5679 255.397 56.5679ZM265.798 32.846C265.284 30.0503 263.989 27.6658 262.036 25.9185C259.919 24.0273 257.144 23.0406 254.061 23.0406C250.998 23.0406 248.264 24.0273 246.146 25.9185C244.194 27.6658 242.919 30.0503 242.405 32.846H265.798Z" fill="white"/>
<path d="M337.274 56.198V33.8327C337.274 30.3587 336.473 27.6864 334.89 25.9185C333.328 24.1507 330.964 23.2668 327.921 23.2668C324.488 23.2668 321.672 24.3768 319.74 26.453C317.808 28.5497 316.78 31.6332 316.78 35.395V56.2185H309.174V33.8327C309.174 26.823 306.008 23.2668 299.759 23.2668C296.326 23.2668 293.51 24.3768 291.578 26.453C289.645 28.5497 288.618 31.6332 288.618 35.395V56.2185H281.012V16.4215H288.412V20.7589C291.413 17.7782 295.73 16.1543 300.725 16.1543C306.687 16.1543 311.394 18.4772 314.087 22.7323C317.335 18.5388 322.577 16.1543 328.682 16.1543C333.656 16.1543 337.603 17.5932 340.419 20.4095C343.359 23.3696 344.901 27.8508 344.88 33.3394V56.1774H337.274V56.198Z" fill="white"/>
<path d="M354.297 69.9296V16.4215H361.697V21.3139C364.986 17.9427 369.467 16.1543 374.75 16.1543C386.241 16.1543 394.567 24.6235 394.567 36.2995C394.567 48.0371 386.241 56.5474 374.75 56.5474C369.57 56.5474 365.171 54.8413 361.923 51.5728V69.909H354.297V69.9296ZM374.422 23.1023C367.021 23.1023 361.841 28.5292 361.841 36.32C361.841 44.1109 367.021 49.5377 374.422 49.5377C381.822 49.5377 387.002 44.1109 387.002 36.32C387.002 28.6525 381.719 23.1023 374.422 23.1023Z" fill="white"/>
<path d="M420.321 56.5679C408.604 56.5679 400.094 48.0576 400.094 36.32C400.094 24.644 408.604 16.1748 420.321 16.1748C425.995 16.1748 431.134 18.2099 434.813 21.91C438.472 25.5896 440.466 30.7081 440.446 36.3405C440.446 48.0576 431.977 56.5679 420.321 56.5679ZM420.321 23.1023C412.921 23.1023 407.741 28.5291 407.741 36.32C407.741 44.1108 412.921 49.5377 420.321 49.5377C427.66 49.5377 432.799 44.1108 432.799 36.32C432.778 28.5291 427.66 23.1023 420.321 23.1023Z" fill="white"/>
<path d="M464.598 56.5679C452.881 56.5679 444.371 48.0576 444.371 36.32C444.371 24.644 452.881 16.1748 464.598 16.1748C476.254 16.1748 484.723 24.644 484.723 36.32C484.723 48.0576 476.254 56.5679 464.598 56.5679ZM464.598 23.1023C457.198 23.1023 452.018 28.5291 452.018 36.32C452.018 44.1108 457.198 49.5377 464.598 49.5377C471.937 49.5377 477.076 44.1108 477.076 36.32C477.076 28.5291 471.958 23.1023 464.598 23.1023Z" fill="white"/>
<path d="M499.996 1.14844H492.391V56.1982H499.996V1.14844Z" fill="white"/>
<path d="M124.706 110.25C124.706 118.849 117.772 125.791 109.183 125.791H15.5236C6.93387 125.791 0 118.849 0 110.25V16.4837C0 7.88416 6.98561 0.942383 15.5236 0.942383H109.183C117.772 0.942383 124.706 7.88416 124.706 16.4837V110.25Z" fill="#2E3349"/>
<path d="M0 63.5225V110.25C0 118.849 6.98561 125.791 15.5753 125.791H109.183C117.772 125.791 124.758 118.849 124.758 110.25V63.5225H0Z" fill="url(#paint0_linear)"/>
<path opacity="0.3" d="M109.909 109.11C109.909 111.026 108.615 112.581 107.011 112.581H90.8665C89.2624 112.581 87.9688 111.026 87.9688 109.11V17.6232C87.9688 15.7065 89.2624 14.1523 90.8665 14.1523H107.011C108.615 14.1523 109.909 15.7065 109.909 17.6232V109.11Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear" x1="62.3768" y1="36.3949" x2="62.3768" y2="156.837" gradientUnits="userSpaceOnUse">
<stop stop-color="#AE61FF"/>
<stop offset="1" stop-color="#13EFD8"/>
</linearGradient>
</defs>
</svg>
</ng-container>
</ng-container>

View File

@@ -0,0 +1,15 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
@Component({
selector: 'app-svg-images',
templateUrl: './svg-images.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SvgImagesComponent {
@Input() name: string;
@Input() class: string;
@Input() style: string;
@Input() width: string;
@Input() height: string;
@Input() viewBox: string;
}

View File

@@ -66,13 +66,16 @@
</ng-template>
</ng-template>
<ng-template #defaultAddress>
<a [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
<a *ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vin.prevout.scriptpubkey_address | shortenString : 16 }}</span>
<span class="d-none d-lg-flex justify-content-start">
<span class="addr-left flex-grow-1" [style]="vin.prevout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vin.prevout.scriptpubkey_address }}</span>
<span *ngIf="vin.prevout.scriptpubkey_address.length > 40" class="addr-right">{{ vin.prevout.scriptpubkey_address | capAddress: 40: 10 }}</span>
</span>
</a>
<ng-template #vinScriptPubkeyType>
{{ vin.prevout.scriptpubkey_type?.toUpperCase() }}
</ng-template>
<div>
<app-address-labels [vin]="vin"></app-address-labels>
</div>

View File

@@ -5,7 +5,7 @@
<div class="col card-wrapper" *ngIf="(network$ | async) !== 'liquid' && (network$ | async) !== 'liquidtestnet'">
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
<div class="card">
<div class="card-body">
<div class="card-body less-padding">
<app-fees-box class="d-block"></app-fees-box>
</div>
</div>
@@ -33,7 +33,7 @@
<div class="col card-wrapper">
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
<div class="card">
<div class="card-body">
<div class="card-body less-padding">
<app-fees-box class="d-block"></app-fees-box>
</div>
</div>

View File

@@ -269,6 +269,9 @@
justify-content: space-around;
padding: 22px 20px;
}
.less-padding {
padding: 20px 20px;
}
}
.retarget-sign {

View File

@@ -2672,48 +2672,216 @@ export const restApiDocsData = [
fragment: "get-blocks",
title: "GET Blocks",
description: {
default: "Returns details on the past 10 blocks. If <code>:startHeight</code> is specified, the past 10 blocks before (and including) <code>:startHeight</code> are returned."
default: "Returns details on the past 15 blocks with fee and mining details in an <code>extras</code> field. If <code>:startHeight</code> is specified, the past 15 blocks before (and including) <code>:startHeight</code> are returned."
},
urlString: "/blocks[/:startHeight]",
showConditions: bitcoinNetworks.concat(liquidNetworks).concat(["bisq"]),
urlString: "/v1/blocks[/:startHeight]",
showConditions: bitcoinNetworks,
showJsExamples: showJsExamplesDefault,
codeExample: {
bisq: {
default: {
codeTemplate: {
curl: `/api/blocks/%{1}/%{2}`,
curl: `/api/v1/blocks/%{1}`,
commonJS: `
const { %{0}: { blocks } } = mempoolJS();
const getBlocks = await blocks.getBlocks({ index: %{1}, length: %{2} });
const getBlocks = await blocks.getBlocks({ startHeight: %{1} });
document.getElementById("result").textContent = JSON.stringify(getBlocks, undefined, 2);
`,
esModule: `
const { %{0}: { blocks } } = mempoolJS();
const getBlocks = await blocks.getBlocks({ index: %{1}, length: %{2} });
const getBlocks = await blocks.getBlocks({ startHeight: %{1} });
console.log(getBlocks);
`,
},
codeSampleMainnet: emptyCodeSample,
codeSampleTestnet: emptyCodeSample,
codeSampleSignet: emptyCodeSample,
codeSampleLiquid: emptyCodeSample,
codeSampleBisq: {
esModule: ['0', '1'],
commonJS: ['0', '1'],
curl: ['0', '1'],
codeSampleMainnet: {
esModule: ['730000'],
commonJS: ['730000'],
curl: ['730000'],
response: `[
{
height: 698771,
time: 1630636953000,
hash: "0000000000000000000a33c6ac863eee8c76ca72435f25d679609c0949ac9374",
previousBlockHash: "00000000000000000001e4184639e5600d3fb4c4e06c2a625e76804c4bc93cb1",
txs: []
}
"id": "0000000000000000000384f28cb3b9cf4377a39cfd6c29ae9466951de38c0529",
"timestamp": 1648829449,
"height": 730000,
"version": 536870912,
"bits": 386521239,
"nonce": 3580664066,
"difficulty": 28587155782195.14,
"merkle_root": "efa344bcd6c0607f93b709515dd6dc5496178112d680338ebea459e3de7b4fbc",
"tx_count": 1627,
"size": 1210916,
"weight": 3993515,
"previousblockhash": "00000000000000000008b6f6fb83f8d74512ef1e0af29e642dd20daddd7d318f",
"extras": {
"coinbaseRaw": "0390230b1362696e616e63652f383038e0006f02cd583765fabe6d6d686355577affaad03015e732428a927a5d2d842471b350394139616bcb4401d804000000000000001a750000c9ad0000",
"medianFee": 11,
"feeRange": [
1,
11,
11,
11,
18,
21,
660
],
"reward": 641321983,
"totalFees": 16321983,
"avgFee": 10038,
"avgFeeRate": 16,
"pool": {
"id": 105,
"name": "Binance Pool",
"slug": "binancepool"
}
}
},
{
"id": "00000000000000000008b6f6fb83f8d74512ef1e0af29e642dd20daddd7d318f",
"timestamp": 1648828946,
"height": 729999,
"version": 793796608,
"bits": 386521239,
"nonce": 3477019455,
"difficulty": 28587155782195.14,
"merkle_root": "d84f9cc1823bd069c505061b1f6faabd809d67ab5354e9f6234312dc4bdb1ecf",
"tx_count": 2574,
"size": 1481957,
"weight": 3993485,
"previousblockhash": "000000000000000000071e6c86c2175aa86817cae2a77acd95372b55c1103d89",
"extras": {
"coinbaseRaw": "038f230b1362696e616e63652f373739d8002900ca5de7a9fabe6d6dda31112c36c10a523154eae76847579755cd4ae558ee2e6f9f200b05dd32a0bf04000000000000006372000000020000",
"medianFee": 17,
"feeRange": [
2,
11,
14,
17,
19,
28,
502
],
"reward": 649090210,
"totalFees": 24090210,
"avgFee": 9362,
"avgFeeRate": 24,
"pool": {
"id": 105,
"name": "Binance Pool",
"slug": "binancepool"
}
}
},
...
]`,
},
codeSampleTestnet: {
esModule: ['2091187'],
commonJS: ['2091187'],
curl: ['2091187'],
response: `[
{
"id": "00000000000000533f63df886281a9fd74da163e84a21445153ff480e5f57970",
"timestamp": 1630641890,
"height": 2091187,
"version": 545259520,
"bits": 436273151,
"nonce": 309403673,
"difficulty": 16777216,
"merkle_root": "4d6df12a4af11bb928c7b2930e0a4d2c3e268c6dc6a07462943ad1c4b6b96468",
"tx_count": 26,
"size": 8390,
"weight": 22985,
"previousblockhash": "0000000000000079103da7d296e1480295df795b7379e7dffd27743e214b0b32",
"extras": {
"coinbaseRaw": "03b3e81f3a205468697320626c6f636b20776173206d696e65642077697468206120636172626f6e206e6567617469766520706f77657220736f75726365201209687a2009092009020de601d7986a040000",
"medianFee": 1,
"feeRange": [
1,
1,
1,
1,
5,
56,
5053
],
"reward": 10547567,
"totalFees": 781942,
"avgFee": 31277,
"avgFeeRate": 143,
"pool": {
"id": 137,
"name": "Unknown",
"slug": "unknown"
}
}
},
...
]`
},
},
codeSampleSignet: {
esModule: ['53783'],
commonJS: ['53783'],
curl: ['53783'],
response: `[
{
"id": "0000010eeacb878340bae34af4e13551413d76a172ec302f7e50b62cb45374f2",
"timestamp": 1630641504,
"height": 53783,
"version": 536870912,
"bits": 503404179,
"nonce": 11753379,
"difficulty": 0.002919030932507782,
"merkle_root": "3063ff3802c920eea68bdc9303957f3e7bfd0a03c93547fd7dad14b77a07d4e8",
"tx_count": 1,
"size": 343,
"weight": 1264,
"previousblockhash": "00000109a7ea774fcc2d173f9a1da9595a47ff401dac67ca9edea149954210fa",
"extras": {
"coinbaseRaw": "0317d200",
"medianFee": 0,
"feeRange": [
0,
0,
0,
0,
0,
0,
0
],
"reward": 5000000000,
"totalFees": 0,
"avgFee": 0,
"avgFeeRate": 0,
"pool": {
"id": 137,
"name": "Unknown",
"slug": "unknown"
}
}
},
...
]`
},
codeSampleLiquid: emptyCodeSample,
codeSampleLiquidTestnet: emptyCodeSample,
codeSampleBisq: emptyCodeSample,
}
}
},
{
type: "endpoint",
category: "blocks",
httpRequestMethod: "GET",
fragment: "get-blocks",
title: "GET Blocks",
description: {
default: "Returns details on the past 10 blocks with fee and mining details in an <code>extras</code> field. If <code>:startHeight</code> is specified, the past 10 blocks before (and including) <code>:startHeight</code> are returned."
},
urlString: "/blocks[/:startHeight]",
showConditions: liquidNetworks,
showJsExamples: showJsExamplesDefault,
codeExample: {
default: {
codeTemplate: {
curl: `/api/blocks/%{1}`,
@@ -2731,75 +2899,9 @@ export const restApiDocsData = [
console.log(getBlocks);
`,
},
codeSampleMainnet: {
esModule: ['698777'],
commonJS: ['698777'],
curl: ['698777'],
response: `[
{
id: "00000000000000000003002915e015c47610c55b6f0228ad62bfcc59b65e67b7",
height: 698777,
version: 536870916,
timestamp: 1630641711,
tx_count: 2327,
size: 1466537,
weight: 3999653,
merkle_root: "023e27dde144eedc65ff3b27c535ebc7dced6c49fe78f94cdf85cf2000608d2f",
previousblockhash: "0000000000000000000701a7f14e362d3f10aa524200db1710ce3bbf0c0f8b75",
mediantime: 1630636986,
nonce: 1926094388,
bits: 386923168,
difficulty: 17615033039278
},
...
]`
},
codeSampleTestnet: {
esModule: ['2091187'],
commonJS: ['2091187'],
curl: ['2091187'],
response: `[
{
id: "00000000000000533f63df886281a9fd74da163e84a21445153ff480e5f57970",
height: 2091187,
version: 545259520,
timestamp: 1630641890,
tx_count: 26,
size: 8390,
weight: 22985,
merkle_root: "4d6df12a4af11bb928c7b2930e0a4d2c3e268c6dc6a07462943ad1c4b6b96468",
previousblockhash: "0000000000000079103da7d296e1480295df795b7379e7dffd27743e214b0b32",
mediantime: 1630639627,
nonce: 309403673,
bits: 436273151,
difficulty: 16777216
},
...
]`
},
codeSampleSignet: {
esModule: ['53783'],
commonJS: ['53783'],
curl: ['53783'],
response: `[
{
id: "0000010eeacb878340bae34af4e13551413d76a172ec302f7e50b62cb45374f2",
height: 53783,
version: 536870912,
timestamp: 1630641504,
tx_count: 1,
size: 343,
weight: 1264,
merkle_root: "3063ff3802c920eea68bdc9303957f3e7bfd0a03c93547fd7dad14b77a07d4e8",
previousblockhash: "00000109a7ea774fcc2d173f9a1da9595a47ff401dac67ca9edea149954210fa",
mediantime: 1630638966,
nonce: 11753379,
bits: 503404179,
difficulty: 0
},
...
]`
},
codeSampleMainnet: emptyCodeSample,
codeSampleTestnet: emptyCodeSample,
codeSampleSignet: emptyCodeSample,
codeSampleLiquid: {
esModule: ['1472246'],
commonJS: ['1472246'],
@@ -2848,144 +2950,79 @@ export const restApiDocsData = [
type: "endpoint",
category: "blocks",
httpRequestMethod: "GET",
fragment: "get-blocks-extras",
title: "GET Blocks Extras",
fragment: "get-blocks",
title: "GET Blocks",
description: {
default: "Returns details on the past 15 blocks with fee and mining details in an <code>extras</code> field. If <code>:startHeight</code> is specified, the past 15 blocks before (and including) <code>:startHeight</code> are returned."
default: "<p>Returns the past <code>n</code> blocks with BSQ transactions starting <code>m</code> blocks ago.</p><p>Assume a block height of 700,000. Query <code>/blocks/0/10</code> for the past 10 blocks before 700,000 with BSQ transactions. Query <code>/blocks/1000/10</code> for the past 10 blocks before 699,000 with BSQ transactions."
},
urlString: "/blocks-extras[/:startHeight]",
showConditions: bitcoinNetworks,
showJsExamples: showJsExamplesDefaultFalse,
urlString: "/blocks/:m/:n",
showConditions: ["bisq"],
showJsExamples: showJsExamplesDefault,
codeExample: {
default: {
codeTemplate: {
curl: `/api/blocks-extras/%{1}`,
commonJS: ``,
esModule: ``
},
codeSampleMainnet: {
esModule: [],
commonJS: [],
curl: ['736915'],
response: `[
{
"extras": {
"reward": 629766074,
"coinbaseTx": {
"vin": [
{
"scriptsig": "03933e0b215c204d41524120506f6f6c205c00000000be82a250e5ef942790d2542ca87d0000"
}
],
"vout": [
{
"scriptpubkey_address": "1A32KFEX7JNPmU1PVjrtiXRrTQcesT3Nf1",
"value": 629766074
}
]
},
"coinbaseRaw": "03933e0b215c204d41524120506f6f6c205c00000000be82a250e5ef942790d2542ca87d0000",
"medianFee": 14,
"feeRange": [
1,
1,
4,
14,
15,
20,
364
],
"totalFees": 4766074,
"avgFee": 5043,
"avgFeeRate": 14,
"pool": {
"id": 115,
"name": "MARA Pool",
"slug": "marapool"
},
"matchRate": 100
},
"id": "00000000000000000000a742ae476dbe2a58c48b193484945c52b05967f2d74c",
"height": 736915,
"version": 541065216,
"timestamp": 1652877171,
"bits": 386466234,
"nonce": 4069175824,
"difficulty": 31251101365711.12,
"merkle_root": "de54fd1adee9f010534e8efbf1244a01528e20dd283c8927026f5442c3e03459",
"tx_count": 946,
"size": 524907,
"weight": 1362339,
"previousblockhash": "000000000000000000070760a253405ca69498464d9f8e9fab2452cbbfc10cbe"
},
{
"extras": {
"reward": 638804415,
"coinbaseTx": {
"vin": [
{
"scriptsig": "03923e0bfabe6d6dc3e96cee3cb68ee52bd31fde8e1f4983a780ea836115788d81a559e03791071f01000000000000001065040008d708c7010000000000007a6d4683012f736c7573682f"
}
],
"vout": [
{
"scriptpubkey_address": "1CK6KHY6MHgYvmRQ4PAafKYDrg1ejbH1cE",
"value": 638804415
}
]
},
"coinbaseRaw": "03923e0bfabe6d6dc3e96cee3cb68ee52bd31fde8e1f4983a780ea836115788d81a559e03791071f01000000000000001065040008d708c7010000000000007a6d4683012f736c7573682f",
"medianFee": 14,
"feeRange": [
1,
1,
2,
14,
15,
20,
347
],
"totalFees": 13804415,
"avgFee": 5287,
"avgFeeRate": 14,
"pool": {
"id": 43,
"name": "SlushPool",
"slug": "slushpool"
},
"matchRate": 100
},
"id": "000000000000000000070760a253405ca69498464d9f8e9fab2452cbbfc10cbe",
"height": 736914,
"version": 555696132,
"timestamp": 1652876939,
"bits": 386466234,
"nonce": 3839610443,
"difficulty": 31251101365711.12,
"merkle_root": "dc6d15f641e7af26dbaf3ee37203155f8053a8755e85f4955d11ea0c54008b16",
"tx_count": 2612,
"size": 1450209,
"weight": 3931749,
"previousblockhash": "00000000000000000002b5b2afc1c62e61e53f966b965a9a8ce99112e24066ae"
},
...
]`,
},
codeSampleTestnet: {
esModule: [],
commonJS: [],
curl: ['2226118'],
response: `[]`
},
codeSampleSignet: {
esModule: [],
commonJS: [],
curl: ['88832'],
response: `[]`
curl: `/api/blocks/%{1}/%{2}`,
commonJS: `
const { %{0}: { blocks } } = mempoolJS();
const getBlocks = await blocks.getBlocks({ index: %{1}, length: %{2} });
document.getElementById("result").textContent = JSON.stringify(getBlocks, undefined, 2);
`,
esModule: `
const { %{0}: { blocks } } = mempoolJS();
const getBlocks = await blocks.getBlocks({ index: %{1}, length: %{2} });
console.log(getBlocks);
`,
},
codeSampleMainnet: emptyCodeSample,
codeSampleTestnet: emptyCodeSample,
codeSampleSignet: emptyCodeSample,
codeSampleLiquid: emptyCodeSample,
codeSampleLiquidTestnet: emptyCodeSample,
codeSampleBisq: emptyCodeSample,
codeSampleBisq: {
esModule: ['0', '5'],
commonJS: ['0', '5'],
curl: ['0', '5'],
response: `[
{
"height": 739030,
"time": 1654203258000,
"hash": "000000000000000000036bc04416ddeec264cbb977a9cd9e454897acb547b601",
"previousBlockHash": "00000000000000000000f49261617b589d76e5e70529ea1d4c16f3e19ddcb8ef",
"txs": [ ... ],
},
{
"height": 739029,
"time": 1654203236000,
"hash": "00000000000000000000f49261617b589d76e5e70529ea1d4c16f3e19ddcb8ef",
"previousBlockHash": "00000000000000000008dd87e9486cd0d71c5d84e452432bab33c2a0cbaa31ce",
"txs": [ ... ],
},
{
"height": 739025,
"time": 1654199569000,
"hash": "000000000000000000021e9ce82dec208af75807f92a9b1d9dae91f2b4d40e24",
"previousBlockHash": "00000000000000000002db644c025a76464b466d25900402452b07213b30c40b",
"txs": [ ... ]
},
{
"height": 739023,
"time": 1654198597000,
"hash": "0000000000000000000702ce10250a46bea4155ca7acb869f3ea92c1e3a68bc5",
"previousBlockHash": "00000000000000000002b3d6c1adc5676262ded84181982f88dbd357b9f9d1ec",
"txs": [ ... ]
},
{
"height": 739020,
"time": 1654197263000,
"hash": "000000000000000000046eb46ad941028381d3534c35658f9c80de0641dbbb31",
"previousBlockHash": "000000000000000000073f1c49b4c4895f3fa6b866d1e21ab8b22f3f9318b42f",
"txs": [ ... ]
}
]`
},
}
}
},

View File

@@ -14,6 +14,7 @@ import { LbtcPegsGraphComponent } from '../components/lbtc-pegs-graph/lbtc-pegs-
import { GraphsComponent } from '../components/graphs/graphs.component';
import { StatisticsComponent } from '../components/statistics/statistics.component';
import { MempoolBlockComponent } from '../components/mempool-block/mempool-block.component';
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
import { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.component';
import { PoolComponent } from '../components/pool/pool.component';
import { TelevisionComponent } from '../components/television/television.component';
@@ -40,6 +41,7 @@ import { CommonModule } from '@angular/common';
BlockFeeRatesGraphComponent,
BlockSizesWeightsGraphComponent,
FeeDistributionGraphComponent,
MempoolBlockOverviewComponent,
IncomingTransactionsGraphComponent,
MempoolGraphComponent,
LbtcPegsGraphComponent,

View File

@@ -21,9 +21,11 @@ export interface WebsocketResponse {
loadingIndicators?: ILoadingIndicators;
backendInfo?: IBackendInfo;
da?: DifficultyAdjustment;
fees?: Recommendedfees;
'track-tx'?: string;
'track-address'?: string;
'track-asset'?: string;
'track-mempool-block'?: number;
'watch-mempool'?: boolean;
'track-bisq-market'?: string;
}
@@ -43,6 +45,16 @@ export interface MempoolBlock {
index: number;
}
export interface MempoolBlockWithTransactions extends MempoolBlock {
transactionIds: string[];
transactions: TransactionStripped[];
}
export interface MempoolBlockDelta {
added: TransactionStripped[],
removed: string[],
}
export interface MempoolInfo {
loaded: boolean; // (boolean) True if the mempool is fully loaded
size: number; // (numeric) Current tx count
@@ -65,3 +77,11 @@ export interface IBackendInfo {
gitCommit: string;
version: string;
}
export interface Recommendedfees {
fastestFee: number;
halfHourFee: number;
hourFee: number;
minimumFee: number;
economyFee: number;
}

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface';
import { IBackendInfo, MempoolBlock, MempoolInfo, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
import { BlockExtended, DifficultyAdjustment, OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
@@ -80,6 +80,8 @@ export class StateService {
bsqPrice$ = new ReplaySubject<number>(1);
mempoolInfo$ = new ReplaySubject<MempoolInfo>(1);
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
txReplaced$ = new Subject<ReplacedTransaction>();
utxoSpent$ = new Subject<object>();
difficultyAdjustment$ = new ReplaySubject<DifficultyAdjustment>(1);
@@ -90,6 +92,7 @@ export class StateService {
previousRetarget$ = new ReplaySubject<number>(1);
backendInfo$ = new ReplaySubject<IBackendInfo>(1);
loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1);
recommendedFees$ = new ReplaySubject<Recommendedfees>(1);
live2Chart$ = new Subject<OptimizedMempoolStats>();

View File

@@ -27,6 +27,8 @@ export class WebsocketService {
private lastWant: string | null = null;
private isTrackingTx = false;
private trackingTxId: string;
private isTrackingMempoolBlock = false;
private trackingMempoolBlock: number;
private latestGitCommit = '';
private onlineCheckTimeout: number;
private onlineCheckTimeoutTwo: number;
@@ -102,6 +104,9 @@ export class WebsocketService {
if (this.isTrackingTx) {
this.startMultiTrackTransaction(this.trackingTxId);
}
if (this.isTrackingMempoolBlock) {
this.startTrackMempoolBlock(this.trackingMempoolBlock);
}
this.stateService.connectionState$.next(2);
}
@@ -157,6 +162,17 @@ export class WebsocketService {
this.websocketSubject.next({ 'track-asset': 'stop' });
}
startTrackMempoolBlock(block: number) {
this.websocketSubject.next({ 'track-mempool-block': block });
this.isTrackingMempoolBlock = true
this.trackingMempoolBlock = block
}
stopTrackMempoolBlock() {
this.websocketSubject.next({ 'track-mempool-block': -1 });
this.isTrackingMempoolBlock = false
}
startTrackBisqMarket(market: string) {
this.websocketSubject.next({ 'track-bisq-market': market });
}
@@ -263,6 +279,10 @@ export class WebsocketService {
this.stateService.difficultyAdjustment$.next(response.da);
}
if (response.fees) {
this.stateService.recommendedFees$.next(response.fees);
}
if (response.backendInfo) {
this.stateService.backendInfo$.next(response.backendInfo);
@@ -289,6 +309,16 @@ export class WebsocketService {
});
}
if (response['projected-block-transactions']) {
if (response['projected-block-transactions'].index == this.trackingMempoolBlock) {
if (response['projected-block-transactions'].blockTransactions) {
this.stateService.mempoolBlockTransactions$.next(response['projected-block-transactions'].blockTransactions);
} else if (response['projected-block-transactions'].delta) {
this.stateService.mempoolBlockDelta$.next(response['projected-block-transactions'].delta);
}
}
}
if (response['live-2h-chart']) {
this.stateService.live2Chart$.next(response['live-2h-chart']);
}

View File

@@ -71,6 +71,7 @@ import { RewardStatsComponent } from '../components/reward-stats/reward-stats.co
import { DataCyDirective } from '../data-cy.directive';
import { LoadingIndicatorComponent } from '../components/loading-indicator/loading-indicator.component';
import { IndexingProgressComponent } from '../components/indexing-progress/indexing-progress.component';
import { SvgImagesComponent } from '../components/svg-images/svg-images.component';
@NgModule({
declarations: [
@@ -136,6 +137,7 @@ import { IndexingProgressComponent } from '../components/indexing-progress/index
RewardStatsComponent,
LoadingIndicatorComponent,
IndexingProgressComponent,
SvgImagesComponent,
],
imports: [
CommonModule,
@@ -228,6 +230,7 @@ import { IndexingProgressComponent } from '../components/indexing-progress/index
RewardStatsComponent,
LoadingIndicatorComponent,
IndexingProgressComponent,
SvgImagesComponent,
]
})
export class SharedModule {

View File

@@ -398,7 +398,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.html</context>
<context context-type="linenumber">13,14</context>
<context context-type="linenumber">14,15</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
@@ -406,7 +406,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">261,263</context>
<context context-type="linenumber">260,262</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
@@ -454,7 +454,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.html</context>
<context context-type="linenumber">11,12</context>
<context context-type="linenumber">12,13</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/difficulty-adjustments-table/difficulty-adjustments-table.component.html</context>
@@ -466,7 +466,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">260,262</context>
<context context-type="linenumber">259,261</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dashboard/dashboard.component.html</context>
@@ -498,11 +498,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.html</context>
<context context-type="linenumber">18,19</context>
<context context-type="linenumber">19,20</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.html</context>
<context context-type="linenumber">28,32</context>
<context context-type="linenumber">29,33</context>
</context-group>
<note priority="1" from="description">Bisq block transactions title</note>
</trans-unit>
@@ -518,7 +518,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.html</context>
<context context-type="linenumber">4,9</context>
<context context-type="linenumber">4,7</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/liquid-master-page/liquid-master-page.component.html</context>
@@ -526,7 +526,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/master-page/master-page.component.html</context>
<context context-type="linenumber">38,40</context>
<context context-type="linenumber">39,41</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool-ranking/pool-ranking.component.html</context>
@@ -699,7 +699,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/about/about.component.html</context>
<context context-type="linenumber">361,365</context>
<context context-type="linenumber">383,387</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dashboard/dashboard.component.html</context>
@@ -833,11 +833,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">338,340</context>
<context context-type="linenumber">337,339</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">349,352</context>
<context context-type="linenumber">348,351</context>
</context-group>
<note priority="1" from="description">BSQ addresses</note>
</trans-unit>
@@ -940,7 +940,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">249,251</context>
<context context-type="linenumber">252,254</context>
</context-group>
</trans-unit>
<trans-unit id="8fe73a4787b8068b2ba61f54ab7e0f9af2ea1fc9" datatype="html">
@@ -969,6 +969,10 @@
<context context-type="sourcefile">src/app/bisq/bisq-transaction/bisq-transaction.component.html</context>
<context context-type="linenumber">111,117</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.html</context>
<context context-type="linenumber">45,47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
<context context-type="linenumber">13,16</context>
@@ -992,7 +996,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">277,278</context>
<context context-type="linenumber">280,281</context>
</context-group>
<note priority="1" from="description">Transaction singular confirmation count</note>
<note priority="1" from="meaning">shared.confirmation-count.singular</note>
@@ -1020,7 +1024,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">278,279</context>
<context context-type="linenumber">281,282</context>
</context-group>
<note priority="1" from="description">Transaction plural confirmation count</note>
<note priority="1" from="meaning">shared.confirmation-count.plural</note>
@@ -1300,7 +1304,7 @@
<source>Become a sponsor ❤️</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/about/about.component.html</context>
<context context-type="linenumber">33,34</context>
<context context-type="linenumber">30,31</context>
</context-group>
<note priority="1" from="description">about.become-a-sponsor</note>
</trans-unit>
@@ -1308,7 +1312,7 @@
<source>Navigate to <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://mempool.space/sponsor&quot; target=&quot;_blank&quot;&gt;"/>https://mempool.space/sponsor<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a&gt;"/> to sponsor</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/about/about.component.html</context>
<context context-type="linenumber">34</context>
<context context-type="linenumber">31</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/sponsor/sponsor.component.html</context>
@@ -1320,7 +1324,7 @@
<source>Enterprise Sponsors 🚀</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/about/about.component.html</context>
<context context-type="linenumber">38,41</context>
<context context-type="linenumber">35,38</context>
</context-group>
<note priority="1" from="description">about.sponsors.enterprise.withRocket</note>
</trans-unit>
@@ -1328,7 +1332,7 @@
<source>Community Sponsors ❤️</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/about/about.component.html</context>
<context context-type="linenumber">159,162</context>
<context context-type="linenumber">181,184</context>
</context-group>
<note priority="1" from="description">about.sponsors.withHeart</note>
</trans-unit>
@@ -1336,7 +1340,7 @@
<source>Self-Hosted Integrations</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/about/about.component.html</context>
<context context-type="linenumber">173,175</context>
<context context-type="linenumber">195,197</context>
</context-group>
<note priority="1" from="description">about.self-hosted-integrations</note>
</trans-unit>
@@ -1344,7 +1348,7 @@
<source>Wallet Integrations</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/about/about.component.html</context>
<context context-type="linenumber">207,209</context>
<context context-type="linenumber">229,231</context>
</context-group>
<note priority="1" from="description">about.wallet-integrations</note>
</trans-unit>
@@ -1352,7 +1356,7 @@
<source>Community Alliances</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/about/about.component.html</context>
<context context-type="linenumber">261,263</context>
<context context-type="linenumber">283,285</context>
</context-group>
<note priority="1" from="description">about.alliances</note>
</trans-unit>
@@ -1360,7 +1364,7 @@
<source>Project Translators</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/about/about.component.html</context>
<context context-type="linenumber">277,279</context>
<context context-type="linenumber">299,301</context>
</context-group>
<note priority="1" from="description">about.translators</note>
</trans-unit>
@@ -1368,7 +1372,7 @@
<source>Project Contributors</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/about/about.component.html</context>
<context context-type="linenumber">291,293</context>
<context context-type="linenumber">313,315</context>
</context-group>
<note priority="1" from="description">about.contributors</note>
</trans-unit>
@@ -1376,7 +1380,7 @@
<source>Project Members</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/about/about.component.html</context>
<context context-type="linenumber">303,305</context>
<context context-type="linenumber">325,327</context>
</context-group>
<note priority="1" from="description">about.project_members</note>
</trans-unit>
@@ -1384,7 +1388,7 @@
<source>Project Maintainers</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/about/about.component.html</context>
<context context-type="linenumber">316,318</context>
<context context-type="linenumber">338,340</context>
</context-group>
<note priority="1" from="description">about.maintainers</note>
</trans-unit>
@@ -1404,7 +1408,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/master-page/master-page.component.html</context>
<context context-type="linenumber">50,53</context>
<context context-type="linenumber">51,54</context>
</context-group>
</trans-unit>
<trans-unit id="address-label.multisig" datatype="html">
@@ -1474,7 +1478,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">285,287</context>
<context context-type="linenumber">288,290</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dashboard/dashboard.component.html</context>
@@ -1678,11 +1682,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">429,431</context>
<context context-type="linenumber">428,430</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">455,457</context>
<context context-type="linenumber">454,456</context>
</context-group>
</trans-unit>
<trans-unit id="aef0d676b15fdae8cb70fc3b089cce7399fde9da" datatype="html">
@@ -1757,7 +1761,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/master-page/master-page.component.html</context>
<context context-type="linenumber">8,9</context>
<context context-type="linenumber">9,10</context>
</context-group>
<note priority="1" from="description">master-page.offline</note>
</trans-unit>
@@ -1773,7 +1777,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/master-page/master-page.component.html</context>
<context context-type="linenumber">9,14</context>
<context context-type="linenumber">10,15</context>
</context-group>
<note priority="1" from="description">master-page.reconnecting</note>
</trans-unit>
@@ -1789,7 +1793,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/master-page/master-page.component.html</context>
<context context-type="linenumber">22,23</context>
<context context-type="linenumber">23,24</context>
</context-group>
<note priority="1" from="description">master-page.layer2-networks-header</note>
</trans-unit>
@@ -1805,7 +1809,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/master-page/master-page.component.html</context>
<context context-type="linenumber">32,34</context>
<context context-type="linenumber">33,35</context>
</context-group>
<note priority="1" from="description">master-page.dashboard</note>
</trans-unit>
@@ -1869,7 +1873,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.html</context>
<context context-type="linenumber">16,17</context>
<context context-type="linenumber">17,18</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
@@ -1877,7 +1881,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">265,267</context>
<context context-type="linenumber">264,266</context>
</context-group>
</trans-unit>
<trans-unit id="8ba8fe810458280a83df7fdf4c614dfc1a826445" datatype="html">
@@ -1904,7 +1908,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.html</context>
<context context-type="linenumber">15,16</context>
<context context-type="linenumber">16,17</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
@@ -1912,7 +1916,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">264,266</context>
<context context-type="linenumber">263,265</context>
</context-group>
</trans-unit>
<trans-unit id="56fa1cd221491b6478998679cba2dc8d55ba330d" datatype="html">
@@ -1947,11 +1951,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.html</context>
<context context-type="linenumber">19,21</context>
<context context-type="linenumber">20,22</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.html</context>
<context context-type="linenumber">32,35</context>
<context context-type="linenumber">33,36</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-graph/mempool-graph.component.ts</context>
@@ -1963,7 +1967,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">267,271</context>
<context context-type="linenumber">266,270</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
@@ -2005,7 +2009,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.ts</context>
<context context-type="linenumber">71</context>
<context context-type="linenumber">75</context>
</context-group>
<note priority="1" from="description">Next Block</note>
</trans-unit>
@@ -2025,7 +2029,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.html</context>
<context context-type="linenumber">16,17</context>
<context context-type="linenumber">17,18</context>
</context-group>
<note priority="1" from="description">block.median-fee</note>
</trans-unit>
@@ -2045,23 +2049,31 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">6</context>
<context context-type="linenumber">16</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">12</context>
<context context-type="linenumber">22</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.html</context>
<context context-type="linenumber">18</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.html</context>
<context context-type="linenumber">17</context>
<context context-type="linenumber">22,25</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.html</context>
<context context-type="linenumber">21,24</context>
<context context-type="linenumber">61,65</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-blocks/mempool-blocks.component.html</context>
@@ -2089,7 +2101,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">269</context>
<context context-type="linenumber">272</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dashboard/dashboard.component.html</context>
@@ -2110,19 +2122,23 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">5,6</context>
<context context-type="linenumber">15,16</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">11,12</context>
<context context-type="linenumber">21,22</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">17,18</context>
<context context-type="linenumber">26,27</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">31,32</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.html</context>
<context context-type="linenumber">17,20</context>
<context context-type="linenumber">18,21</context>
</context-group>
<note priority="1" from="description">Transaction fee tooltip</note>
</trans-unit>
@@ -2138,7 +2154,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.html</context>
<context context-type="linenumber">24,25</context>
<context context-type="linenumber">25,26</context>
</context-group>
<note priority="1" from="description">Total fees in a block</note>
<note priority="1" from="meaning">block.total-fees</note>
@@ -2260,7 +2276,7 @@
<source>Pool</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.html</context>
<context context-type="linenumber">12,13</context>
<context context-type="linenumber">13,14</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool-ranking/pool-ranking.component.html</context>
@@ -2276,7 +2292,7 @@
<source>Mined</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.html</context>
<context context-type="linenumber">14,15</context>
<context context-type="linenumber">15,16</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
@@ -2284,7 +2300,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">262,263</context>
<context context-type="linenumber">261,262</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dashboard/dashboard.component.html</context>
@@ -2296,7 +2312,7 @@
<source>TXs</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/blocks-list/blocks-list.component.html</context>
<context context-type="linenumber">18</context>
<context context-type="linenumber">19</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
@@ -2304,7 +2320,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">267,269</context>
<context context-type="linenumber">266,268</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/dashboard/dashboard.component.html</context>
@@ -2441,40 +2457,52 @@
</context-group>
<note priority="1" from="description">difficulty-box.next-halving</note>
</trans-unit>
<trans-unit id="f2385584215e9b925b6347de866110d5e07d3a35" datatype="html">
<source>Low priority</source>
<trans-unit id="eef30290726d3d569232f4c136082bb9daaf490b" datatype="html">
<source>No Priority</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">4,5</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">27,30</context>
</context-group>
<note priority="1" from="description">fees-box.low-priority</note>
</trans-unit>
<trans-unit id="0ba9d74c1d31a9d98829892f40334a22624564b8" datatype="html">
<source>Medium priority</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">10,11</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">34,37</context>
</context-group>
<note priority="1" from="description">fees-box.medium-priority</note>
</trans-unit>
<trans-unit id="f80dd1511a91af4e592a538f95fa29d24907176f" datatype="html">
<source>High priority</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">16,17</context>
<context context-type="linenumber">4,7</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">41,44</context>
</context-group>
<note priority="1" from="description">fees-box.no-priority</note>
</trans-unit>
<trans-unit id="29949587189ee02db19274db4ac656913cb243c3" datatype="html">
<source>Low Priority</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">8,9</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">45,46</context>
</context-group>
<note priority="1" from="description">fees-box.low-priority</note>
</trans-unit>
<trans-unit id="ee847b69ef2dc81bb3e9b8cd30f02f8d63adbe07" datatype="html">
<source>Medium Priority</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">9,11</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">46,48</context>
</context-group>
<note priority="1" from="description">fees-box.medium-priority</note>
</trans-unit>
<trans-unit id="d1d0bb0a34b216be66137562a0b18eaaca546113" datatype="html">
<source>High Priority</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">10,15</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/fees-box/fees-box.component.html</context>
<context context-type="linenumber">47,51</context>
</context-group>
<note priority="1" from="description">fees-box.high-priority</note>
</trans-unit>
<trans-unit id="926c571b25cca7e2a294619f145960c0cd3848b6" datatype="html">
@@ -2644,7 +2672,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/master-page/master-page.component.html</context>
<context context-type="linenumber">41,43</context>
<context context-type="linenumber">42,44</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/statistics/statistics.component.ts</context>
@@ -2656,7 +2684,7 @@
<source>Mining Dashboard</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/master-page/master-page.component.html</context>
<context context-type="linenumber">35,37</context>
<context context-type="linenumber">36,38</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mining-dashboard/mining-dashboard.component.ts</context>
@@ -2668,7 +2696,7 @@
<source>TV view</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/master-page/master-page.component.html</context>
<context context-type="linenumber">44,46</context>
<context context-type="linenumber">45,47</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/television/television.component.ts</context>
@@ -2680,7 +2708,7 @@
<source>Documentation</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/master-page/master-page.component.html</context>
<context context-type="linenumber">47,49</context>
<context context-type="linenumber">48,50</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/docs/docs/docs.component.html</context>
@@ -2692,22 +2720,95 @@
<source>Fee span</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.html</context>
<context context-type="linenumber">20,21</context>
<context context-type="linenumber">21,22</context>
</context-group>
<note priority="1" from="description">mempool-block.fee-span</note>
</trans-unit>
<trans-unit id="bc4c61d3713989e3c8c6610fca3ea1ca1cb19edb" datatype="html">
<source>Value</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.html</context>
<context context-type="linenumber">51,54</context>
</context-group>
<note priority="1" from="description">Transaction value</note>
<note priority="1" from="meaning">transaction.value</note>
</trans-unit>
<trans-unit id="cb1b52c13b95fa29ea4044f2bbe0ac623b890c80" datatype="html">
<source>Fee</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.html</context>
<context context-type="linenumber">56</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
<context context-type="linenumber">386</context>
</context-group>
<note priority="1" from="description">Transaction fee</note>
<note priority="1" from="meaning">transaction.fee</note>
</trans-unit>
<trans-unit id="83b8b9dd1ed416447cf8438460a37b0ddeb2677c" datatype="html">
<source>sat</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.html</context>
<context context-type="linenumber">56,57</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
<context context-type="linenumber">386,387</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">272,273</context>
</context-group>
<note priority="1" from="description">sat</note>
<note priority="1" from="meaning">shared.sat</note>
</trans-unit>
<trans-unit id="3e322ffba6477484e0dd2e65650fdd70322ea6d0" datatype="html">
<source>Fee rate</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.html</context>
<context context-type="linenumber">59,61</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
<context context-type="linenumber">154,158</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
<context context-type="linenumber">389,391</context>
</context-group>
<note priority="1" from="description">Transaction fee rate</note>
<note priority="1" from="meaning">transaction.fee-rate</note>
</trans-unit>
<trans-unit id="54c58b39481f1749fce23fa8a77f616eee84d761" datatype="html">
<source>Virtual size</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.html</context>
<context context-type="linenumber">65,68</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
<context context-type="linenumber">153,155</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
<context context-type="linenumber">216,219</context>
</context-group>
<note priority="1" from="description">Transaction Virtual Size</note>
<note priority="1" from="meaning">transaction.vsize</note>
</trans-unit>
<trans-unit id="mempool-block.stack.of.blocks" datatype="html">
<source>Stack of <x id="INTERPOLATION" equiv-text="blocksInBlock"/> mempool blocks</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.ts</context>
<context context-type="linenumber">73</context>
<context context-type="linenumber">77</context>
</context-group>
</trans-unit>
<trans-unit id="mempool-block.block.no" datatype="html">
<source>Mempool block <x id="INTERPOLATION" equiv-text="this.mempoolBlockIndex + 1"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/mempool-block/mempool-block.component.ts</context>
<context context-type="linenumber">75</context>
<context context-type="linenumber">79</context>
</context-group>
</trans-unit>
<trans-unit id="2348971518300945764" datatype="html">
@@ -2850,11 +2951,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">321,323</context>
<context context-type="linenumber">320,322</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">329,331</context>
<context context-type="linenumber">328,330</context>
</context-group>
<note priority="1" from="description">mining.tags</note>
</trans-unit>
@@ -2870,11 +2971,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">148,151</context>
<context context-type="linenumber">151,154</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">259,261</context>
<context context-type="linenumber">262,264</context>
</context-group>
<note priority="1" from="description">show-all</note>
</trans-unit>
@@ -2898,11 +2999,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">368,370</context>
<context context-type="linenumber">367,369</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">395,397</context>
<context context-type="linenumber">394,396</context>
</context-group>
<note priority="1" from="description">mining.hashrate-24h</note>
</trans-unit>
@@ -2918,11 +3019,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">373,374</context>
<context context-type="linenumber">372,373</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">399,400</context>
<context context-type="linenumber">398,399</context>
</context-group>
<note priority="1" from="description">mining.estimated</note>
</trans-unit>
@@ -2938,11 +3039,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">374,375</context>
<context context-type="linenumber">373,374</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">400,401</context>
<context context-type="linenumber">399,400</context>
</context-group>
<note priority="1" from="description">mining.reported</note>
</trans-unit>
@@ -2958,11 +3059,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">375,378</context>
<context context-type="linenumber">374,377</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">401,404</context>
<context context-type="linenumber">400,403</context>
</context-group>
<note priority="1" from="description">mining.luck</note>
</trans-unit>
@@ -2978,11 +3079,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">421,423</context>
<context context-type="linenumber">420,422</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">448,450</context>
<context context-type="linenumber">447,449</context>
</context-group>
<note priority="1" from="description">mining.mined-blocks</note>
</trans-unit>
@@ -3018,7 +3119,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/pool/pool.component.html</context>
<context context-type="linenumber">263,265</context>
<context context-type="linenumber">262,264</context>
</context-group>
<note priority="1" from="description">latest-blocks.coinbasetag</note>
</trans-unit>
@@ -3425,7 +3526,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">281,284</context>
<context context-type="linenumber">284,287</context>
</context-group>
<note priority="1" from="description">Transaction unconfirmed state</note>
<note priority="1" from="meaning">transaction.unconfirmed</note>
@@ -3457,32 +3558,6 @@
<note priority="1" from="description">Transaction ETA in several hours or more</note>
<note priority="1" from="meaning">transaction.eta.in-several-hours</note>
</trans-unit>
<trans-unit id="54c58b39481f1749fce23fa8a77f616eee84d761" datatype="html">
<source>Virtual size</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
<context context-type="linenumber">153,155</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
<context context-type="linenumber">216,219</context>
</context-group>
<note priority="1" from="description">Transaction Virtual Size</note>
<note priority="1" from="meaning">transaction.vsize</note>
</trans-unit>
<trans-unit id="3e322ffba6477484e0dd2e65650fdd70322ea6d0" datatype="html">
<source>Fee rate</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
<context context-type="linenumber">154,158</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
<context context-type="linenumber">389,391</context>
</context-group>
<note priority="1" from="description">Transaction fee rate</note>
<note priority="1" from="meaning">transaction.fee-rate</note>
</trans-unit>
<trans-unit id="dd230222e3ae689913445ce93b6ae3f7cce7458b" datatype="html">
<source>Descendant</source>
<context-group purpose="location">
@@ -3525,28 +3600,6 @@
</context-group>
<note priority="1" from="description">transaction.error.waiting-for-it-to-appear</note>
</trans-unit>
<trans-unit id="cb1b52c13b95fa29ea4044f2bbe0ac623b890c80" datatype="html">
<source>Fee</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
<context context-type="linenumber">386</context>
</context-group>
<note priority="1" from="description">Transaction fee</note>
<note priority="1" from="meaning">transaction.fee</note>
</trans-unit>
<trans-unit id="83b8b9dd1ed416447cf8438460a37b0ddeb2677c" datatype="html">
<source>sat</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction.component.html</context>
<context context-type="linenumber">386,387</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">269,270</context>
</context-group>
<note priority="1" from="description">sat</note>
<note priority="1" from="meaning">shared.sat</note>
</trans-unit>
<trans-unit id="eb1737af67381ce6f0b347038bb4c65b3deb84be" datatype="html">
<source>Effective fee rate</source>
<context-group purpose="location">
@@ -3584,7 +3637,7 @@
<source>ScriptSig (ASM)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">104,106</context>
<context context-type="linenumber">107,109</context>
</context-group>
<note priority="1" from="description">ScriptSig (ASM)</note>
<note priority="1" from="meaning">transactions-list.scriptsig.asm</note>
@@ -3593,7 +3646,7 @@
<source>ScriptSig (HEX)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">108,111</context>
<context context-type="linenumber">111,114</context>
</context-group>
<note priority="1" from="description">ScriptSig (HEX)</note>
<note priority="1" from="meaning">transactions-list.scriptsig.hex</note>
@@ -3602,7 +3655,7 @@
<source>Witness</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">113,115</context>
<context context-type="linenumber">116,118</context>
</context-group>
<note priority="1" from="description">transactions-list.witness</note>
</trans-unit>
@@ -3610,7 +3663,7 @@
<source>P2SH redeem script</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">117,118</context>
<context context-type="linenumber">120,121</context>
</context-group>
<note priority="1" from="description">transactions-list.p2sh-redeem-script</note>
</trans-unit>
@@ -3618,7 +3671,7 @@
<source>P2TR tapscript</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">121,123</context>
<context context-type="linenumber">124,126</context>
</context-group>
<note priority="1" from="description">transactions-list.p2tr-tapscript</note>
</trans-unit>
@@ -3626,7 +3679,7 @@
<source>P2WSH witness script</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">123,125</context>
<context context-type="linenumber">126,128</context>
</context-group>
<note priority="1" from="description">transactions-list.p2wsh-witness-script</note>
</trans-unit>
@@ -3634,7 +3687,7 @@
<source>nSequence</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">128,130</context>
<context context-type="linenumber">131,133</context>
</context-group>
<note priority="1" from="description">transactions-list.nsequence</note>
</trans-unit>
@@ -3642,7 +3695,7 @@
<source>Previous output script</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">133,134</context>
<context context-type="linenumber">136,137</context>
</context-group>
<note priority="1" from="description">transactions-list.previous-output-script</note>
</trans-unit>
@@ -3650,7 +3703,7 @@
<source>Previous output type</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">137,138</context>
<context context-type="linenumber">140,141</context>
</context-group>
<note priority="1" from="description">transactions-list.previous-output-type</note>
</trans-unit>
@@ -3658,7 +3711,7 @@
<source>Peg-out to <x id="START_TAG_NG_CONTAINER" ctype="x-ng_container" equiv-text="&lt;ng-container *ngTemplateOutlet=&quot;pegOutLink&quot;&gt;"/><x id="CLOSE_TAG_NG_CONTAINER" ctype="x-ng_container" equiv-text="&lt;/ng-contain"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">176,177</context>
<context context-type="linenumber">179,180</context>
</context-group>
<note priority="1" from="description">transactions-list.peg-out-to</note>
</trans-unit>
@@ -3666,7 +3719,7 @@
<source>ScriptPubKey (ASM)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">237,239</context>
<context context-type="linenumber">240,242</context>
</context-group>
<note priority="1" from="description">ScriptPubKey (ASM)</note>
<note priority="1" from="meaning">transactions-list.scriptpubkey.asm</note>
@@ -3675,7 +3728,7 @@
<source>ScriptPubKey (HEX)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">241,244</context>
<context context-type="linenumber">244,247</context>
</context-group>
<note priority="1" from="description">ScriptPubKey (HEX)</note>
<note priority="1" from="meaning">transactions-list.scriptpubkey.hex</note>
@@ -3684,7 +3737,7 @@
<source>Show all inputs to reveal fee data</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
<context context-type="linenumber">271,274</context>
<context context-type="linenumber">274,277</context>
</context-group>
<note priority="1" from="description">transactions-list.load-to-reveal-fee-info</note>
</trans-unit>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 856 KiB

View File

@@ -243,6 +243,10 @@ body {
}
}
.table-fixed {
table-layout: fixed;
}
.close {
color: #fff;
}