Compare commits
1 Commits
mononaut/m
...
simon/e2e-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79fce5a269 |
@@ -3,8 +3,7 @@ import * as WebSocket from 'ws';
|
|||||||
import {
|
import {
|
||||||
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
|
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
|
||||||
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
|
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
|
||||||
MempoolDelta, MempoolDeltaTxids,
|
MempoolDelta, MempoolDeltaTxids
|
||||||
TransactionCompressed
|
|
||||||
} from '../mempool.interfaces';
|
} from '../mempool.interfaces';
|
||||||
import blocks from './blocks';
|
import blocks from './blocks';
|
||||||
import memPool from './mempool';
|
import memPool from './mempool';
|
||||||
@@ -316,7 +315,6 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
if (parsedMessage && parsedMessage['track-mempool-block'] !== undefined) {
|
if (parsedMessage && parsedMessage['track-mempool-block'] !== undefined) {
|
||||||
if (Number.isInteger(parsedMessage['track-mempool-block']) && parsedMessage['track-mempool-block'] >= 0) {
|
if (Number.isInteger(parsedMessage['track-mempool-block']) && parsedMessage['track-mempool-block'] >= 0) {
|
||||||
client['track-mempool-blocks'] = undefined;
|
|
||||||
const index = parsedMessage['track-mempool-block'];
|
const index = parsedMessage['track-mempool-block'];
|
||||||
client['track-mempool-block'] = index;
|
client['track-mempool-block'] = index;
|
||||||
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||||
@@ -326,31 +324,7 @@ class WebsocketHandler {
|
|||||||
blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
|
blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
client['track-mempool-block'] = undefined;
|
client['track-mempool-block'] = null;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedMessage && parsedMessage['track-mempool-blocks'] !== undefined) {
|
|
||||||
if (parsedMessage['track-mempool-blocks'].length > 0) {
|
|
||||||
client['track-mempool-block'] = undefined;
|
|
||||||
const indices: number[] = [];
|
|
||||||
const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions();
|
|
||||||
const updates: { index: number, sequence: number, blockTransactions: TransactionCompressed[] }[] = [];
|
|
||||||
for (const i of parsedMessage['track-mempool-blocks']) {
|
|
||||||
const index = parseInt(i);
|
|
||||||
if (Number.isInteger(index) && index >= 0) {
|
|
||||||
indices.push(index);
|
|
||||||
updates.push({
|
|
||||||
index: index,
|
|
||||||
sequence: this.mempoolSequence,
|
|
||||||
blockTransactions: (mBlocksWithTransactions[index]?.transactions || []).map(mempoolBlocks.compressTx),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
client['track-mempool-blocks'] = indices;
|
|
||||||
response['projected-block-transactions'] = JSON.stringify(updates);
|
|
||||||
} else {
|
|
||||||
client['track-mempool-blocks'] = undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -934,19 +908,6 @@ class WebsocketHandler {
|
|||||||
delta: mBlockDeltas[index],
|
delta: mBlockDeltas[index],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (client['track-mempool-blocks']?.length && memPool.isInSync()) {
|
|
||||||
const indices = client['track-mempool-blocks'];
|
|
||||||
const updates: string[] = [];
|
|
||||||
for (const index of indices) {
|
|
||||||
if (mBlockDeltas[index]) {
|
|
||||||
updates.push(getCachedResponse(`projected-block-transactions-${index}`, {
|
|
||||||
index: index,
|
|
||||||
sequence: this.mempoolSequence,
|
|
||||||
delta: mBlockDeltas[index],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response['projected-block-transactions'] = '[' + updates.join(',') + ']';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client['track-rbf'] === 'all' && rbfReplacements) {
|
if (client['track-rbf'] === 'all' && rbfReplacements) {
|
||||||
@@ -1335,27 +1296,6 @@ class WebsocketHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (client['track-mempool-blocks']?.length && memPool.isInSync()) {
|
|
||||||
const indices = client['track-mempool-blocks'];
|
|
||||||
const updates: string[] = [];
|
|
||||||
for (const index of indices) {
|
|
||||||
if (mBlockDeltas && mBlockDeltas[index] && mBlocksWithTransactions[index]?.transactions?.length) {
|
|
||||||
if (mBlockDeltas[index].added.length > (mBlocksWithTransactions[index]?.transactions.length / 2)) {
|
|
||||||
updates.push(getCachedResponse(`projected-block-transactions-full-${index}`, {
|
|
||||||
index: index,
|
|
||||||
sequence: this.mempoolSequence,
|
|
||||||
blockTransactions: mBlocksWithTransactions[index].transactions.map(mempoolBlocks.compressTx),
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
updates.push(getCachedResponse(`projected-block-transactions-delta-${index}`, {
|
|
||||||
index: index,
|
|
||||||
sequence: this.mempoolSequence,
|
|
||||||
delta: mBlockDeltas[index],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response['projected-block-transactions'] = '[' + updates.join(',') + ']';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client['track-mempool-txids']) {
|
if (client['track-mempool-txids']) {
|
||||||
|
|||||||
@@ -594,4 +594,63 @@ describe('Mainnet', () => {
|
|||||||
} else {
|
} else {
|
||||||
it.skip(`Tests cannot be run on the selected BASE_MODULE ${baseModule}`);
|
it.skip(`Tests cannot be run on the selected BASE_MODULE ${baseModule}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe('Accelerated Transactions', () => {
|
||||||
|
describe('Unconfirmed Accelerated Transaction', () => {
|
||||||
|
before(() => {
|
||||||
|
cy.intercept('/api/tx/40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a', {
|
||||||
|
fixture: 'accelerated_tx.json'
|
||||||
|
}).as('tx');
|
||||||
|
|
||||||
|
cy.intercept('/api/v1/cpfp/40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a', {
|
||||||
|
fixture: 'accelerated_cpfp.json'
|
||||||
|
}).as('accelerated_cpfp');
|
||||||
|
|
||||||
|
cy.intercept('/api/v1/transaction-times?txId%5B%5D=40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a', {
|
||||||
|
body: '[1723416086]',
|
||||||
|
}).as('transaction-time');
|
||||||
|
|
||||||
|
cy.intercept('https://mempool.space/api/v1/services/accelerator/accelerations/history', {
|
||||||
|
fixture: 'accelerated_history.json'
|
||||||
|
}).as('history');
|
||||||
|
|
||||||
|
cy.viewport('macbook-16');
|
||||||
|
cy.mockMempoolSocket();
|
||||||
|
cy.visit('/tx/40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a');
|
||||||
|
|
||||||
|
emitMempoolInfo({
|
||||||
|
'params': {
|
||||||
|
command: 'txPosition'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cy.waitForSkeletonGone();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows unconfirmed accelerated transaction properly', () => {
|
||||||
|
cy.get('.badge-accelerated').should('exist');
|
||||||
|
cy.get('[data-cy="active-acceleration-box"]').should('exist');
|
||||||
|
cy.get('[data-cy="active-acceleration-box"] > table > tbody > :nth-child(1) .oobFees').invoke('text').should('contain', `15.5 `);
|
||||||
|
cy.get('[data-cy="tx-fee-delta"]').invoke('text').should('contain', `3,000`);
|
||||||
|
cy.get('#acceleration-timeline').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
// currently doesn't work due to 'accelerations/history' endpoint not being intercepted
|
||||||
|
it.skip('properly render accelerated transacion as it confirms', () => {
|
||||||
|
emitMempoolInfo({
|
||||||
|
'params': {
|
||||||
|
command: 'txPositionConfirmed'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.wait(1000);
|
||||||
|
|
||||||
|
cy.get('.badge-accelerated').should('exist');
|
||||||
|
cy.get('[data-cy="active-acceleration-box"]').should('not.exist');
|
||||||
|
cy.get('[data-cy="fee-rate"]').invoke('text').should('contain', `2.17 `);
|
||||||
|
cy.get('[data-cy="tx-fee-delta"]').invoke('text').should('contain', `39`);
|
||||||
|
cy.get('#acceleration-timeline').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
20
frontend/cypress/fixtures/accelerated_cpfp.json
Normal file
20
frontend/cypress/fixtures/accelerated_cpfp.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"ancestors": [],
|
||||||
|
"bestDescendant": null,
|
||||||
|
"descendants": [],
|
||||||
|
"effectiveFeePerVsize": 15.452914798206278,
|
||||||
|
"sigops": 4,
|
||||||
|
"fee": 446,
|
||||||
|
"adjustedVsize": 223,
|
||||||
|
"acceleration": true,
|
||||||
|
"acceleratedBy": [
|
||||||
|
111,
|
||||||
|
43,
|
||||||
|
102,
|
||||||
|
112,
|
||||||
|
142,
|
||||||
|
115
|
||||||
|
],
|
||||||
|
"acceleratedAt": 1723417553,
|
||||||
|
"feeDelta": 3000
|
||||||
|
}
|
||||||
24
frontend/cypress/fixtures/accelerated_history.json
Normal file
24
frontend/cypress/fixtures/accelerated_history.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"txid": "40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a",
|
||||||
|
"status": "completed",
|
||||||
|
"added": 1723417553,
|
||||||
|
"lastUpdated": 1723424127,
|
||||||
|
"effectiveFee": 446,
|
||||||
|
"effectiveVsize": 223,
|
||||||
|
"feeDelta": 3000,
|
||||||
|
"blockHash": "000000000000000000005bc0a822da172e43c687428cc268177ad27d636f3059",
|
||||||
|
"blockHeight": 856387,
|
||||||
|
"bidBoost": 39,
|
||||||
|
"boostVersion": "v2",
|
||||||
|
"pools": [
|
||||||
|
111,
|
||||||
|
43,
|
||||||
|
102,
|
||||||
|
112,
|
||||||
|
142,
|
||||||
|
115
|
||||||
|
],
|
||||||
|
"minedByPoolUniqueId": 111
|
||||||
|
}
|
||||||
|
]
|
||||||
48
frontend/cypress/fixtures/accelerated_position.json
Normal file
48
frontend/cypress/fixtures/accelerated_position.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"txPosition": {
|
||||||
|
"txid": "40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a",
|
||||||
|
"position": {
|
||||||
|
"block": 0,
|
||||||
|
"vsize": 37321.5,
|
||||||
|
"accelerated": true
|
||||||
|
},
|
||||||
|
"accelerationPositions": [
|
||||||
|
{
|
||||||
|
"block": 0,
|
||||||
|
"vsize": 37321.5,
|
||||||
|
"poolId": 111,
|
||||||
|
"pool": "Foundry USA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block": 0,
|
||||||
|
"vsize": 37321.5,
|
||||||
|
"poolId": 43,
|
||||||
|
"pool": "Braiins Pool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block": 0,
|
||||||
|
"vsize": 37321.5,
|
||||||
|
"poolId": 102,
|
||||||
|
"pool": "SpiderPool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block": 0,
|
||||||
|
"vsize": 37321.5,
|
||||||
|
"poolId": 112,
|
||||||
|
"pool": "SBI Crypto"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block": 0,
|
||||||
|
"vsize": 37321.5,
|
||||||
|
"poolId": 142,
|
||||||
|
"pool": "OCEAN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block": 0,
|
||||||
|
"vsize": 37321.5,
|
||||||
|
"poolId": 115,
|
||||||
|
"pool": "MARA Pool"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"txConfirmed": "40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a",
|
||||||
|
"block":{
|
||||||
|
"id": "000000000000000000014cc3d86b7c096ef92aca180e3cf27d72e34ce944caed",
|
||||||
|
"height": 837051,
|
||||||
|
"version": 821051392,
|
||||||
|
"timestamp": 1723452588,
|
||||||
|
"bits": 386079422,
|
||||||
|
"nonce": 2215159619,
|
||||||
|
"difficulty": 90666502495565.78,
|
||||||
|
"merkle_root": "207ad51f6c1150f63fcd043eb1b4624b77ac70558594317e989c1109fbb47c47",
|
||||||
|
"tx_count": 2284,
|
||||||
|
"size": 1490522,
|
||||||
|
"weight": 3993155,
|
||||||
|
"previousblockhash": "00000000000000000002b8a66307c997aa27bf99a384ceb7cfe5f29576eddb26",
|
||||||
|
"mediantime": 1723450608,
|
||||||
|
"stale": false,
|
||||||
|
"extras": {
|
||||||
|
"reward": 319417632,
|
||||||
|
"coinbaseRaw": "0378110d04adccb9662f466f756e6472792055534120506f6f6c202364726f70676f6c642f2c08727fca05000000000000",
|
||||||
|
"orphans": [],
|
||||||
|
"medianFee": 4.021446911342697,
|
||||||
|
"feeRange": [
|
||||||
|
3.1,
|
||||||
|
3.4184397163120566,
|
||||||
|
3.998624011007912,
|
||||||
|
4.444976076555024,
|
||||||
|
5.382978723404255,
|
||||||
|
11.62814371257485,
|
||||||
|
468.75
|
||||||
|
],
|
||||||
|
"totalFees": 6917632,
|
||||||
|
"avgFee": 3030,
|
||||||
|
"avgFeeRate": 6,
|
||||||
|
"utxoSetChange": -2647,
|
||||||
|
"avgTxSize": 652.44,
|
||||||
|
"totalInputs": 8544,
|
||||||
|
"totalOutputs": 5897,
|
||||||
|
"totalOutputAmt": 2950130527407,
|
||||||
|
"segwitTotalTxs": 2084,
|
||||||
|
"segwitTotalSize": 1137877,
|
||||||
|
"segwitTotalWeight": 2582683,
|
||||||
|
"feePercentiles": null,
|
||||||
|
"virtualSize": 998288.75,
|
||||||
|
"coinbaseAddress": "bc1p8k4v4xuz55dv49svzjg43qjxq2whur7ync9tm0xgl5t4wjl9ca9snxgmlt",
|
||||||
|
"coinbaseAddresses": [
|
||||||
|
"bc1p8k4v4xuz55dv49svzjg43qjxq2whur7ync9tm0xgl5t4wjl9ca9snxgmlt",
|
||||||
|
"bc1qxhmdufsvnuaaaer4ynz88fspdsxq2h9e9cetdj"
|
||||||
|
],
|
||||||
|
"coinbaseSignature": "OP_PUSHNUM_1 OP_PUSHBYTES_32 3daaca9b82a51aca960c1491588246029d7e0fc49e0abdbcc8fd17574be5c74b",
|
||||||
|
"coinbaseSignatureAscii": "f/Foundry USA Pool #dropgold/",
|
||||||
|
"header": "0040f03026dbed7695f2e5cfb7ce84a399bf27aa97c90763a6b802000000000000000000477cb4fb09119c987e3194855570ac774b62b4b13e04cd3ff650116c1fd57a20acccb966be1a031743a70884",
|
||||||
|
"utxoSetSize": null,
|
||||||
|
"totalInputAmt": null,
|
||||||
|
"pool": {
|
||||||
|
"id": 111,
|
||||||
|
"name": "Foundry USA",
|
||||||
|
"slug": "foundryusa"
|
||||||
|
},
|
||||||
|
"matchRate": 100,
|
||||||
|
"expectedFees": 6957093,
|
||||||
|
"expectedWeight": 3991895,
|
||||||
|
"similarity": 0.9907343565880212
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
frontend/cypress/fixtures/accelerated_tx.json
Normal file
45
frontend/cypress/fixtures/accelerated_tx.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"txid": "40ba6b3c4ce73e3ba0160c137b1cc6c2c7333a2b9c19537b61ee8a8aaf095e0a",
|
||||||
|
"version": 1,
|
||||||
|
"locktime": 0,
|
||||||
|
"vin": [
|
||||||
|
{
|
||||||
|
"txid": "7c6e17739d7225d097db1f08df17d06dc712dc0951f266db1070939b85b5e8e7",
|
||||||
|
"vout": 0,
|
||||||
|
"prevout": {
|
||||||
|
"scriptpubkey": "76a914fb706ea28ba8f83e3cfa2fa1f3f01a6a613b94ca88ac",
|
||||||
|
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb706ea28ba8f83e3cfa2fa1f3f01a6a613b94ca OP_EQUALVERIFY OP_CHECKSIG",
|
||||||
|
"scriptpubkey_type": "p2pkh",
|
||||||
|
"scriptpubkey_address": "1PvVJ5FvkNnsatmD4nfkb6j59CjKq7dxxy",
|
||||||
|
"value": 16610556
|
||||||
|
},
|
||||||
|
"scriptsig": "483045022100811726483f9c91dd91aa136c6ba4e97e6db79ef7026aa4fdd4216ea6a954f91a0220508b7fdf4078bf82114f7cfed5090b77114dec19b122870a34e562689441399d01210275f84bf0270b233f83be9b1ba6549e3281a133bfd93b24e1c16d80c4e742f09e",
|
||||||
|
"scriptsig_asm": "OP_PUSHBYTES_72 3045022100811726483f9c91dd91aa136c6ba4e97e6db79ef7026aa4fdd4216ea6a954f91a0220508b7fdf4078bf82114f7cfed5090b77114dec19b122870a34e562689441399d01 OP_PUSHBYTES_33 0275f84bf0270b233f83be9b1ba6549e3281a133bfd93b24e1c16d80c4e742f09e",
|
||||||
|
"is_coinbase": false,
|
||||||
|
"sequence": 4294967295
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vout": [
|
||||||
|
{
|
||||||
|
"scriptpubkey": "0014ce6c0bb00482016d12657174b6468cd01df6421e",
|
||||||
|
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 ce6c0bb00482016d12657174b6468cd01df6421e",
|
||||||
|
"scriptpubkey_type": "v0_p2wpkh",
|
||||||
|
"scriptpubkey_address": "bc1qeekqhvqysgqk6yn9w96tv35v6qwlvss7vuvtj0",
|
||||||
|
"value": 6796193
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scriptpubkey": "76a914fb706ea28ba8f83e3cfa2fa1f3f01a6a613b94ca88ac",
|
||||||
|
"scriptpubkey_asm": "OP_DUP OP_HASH160 OP_PUSHBYTES_20 fb706ea28ba8f83e3cfa2fa1f3f01a6a613b94ca OP_EQUALVERIFY OP_CHECKSIG",
|
||||||
|
"scriptpubkey_type": "p2pkh",
|
||||||
|
"scriptpubkey_address": "1PvVJ5FvkNnsatmD4nfkb6j59CjKq7dxxy",
|
||||||
|
"value": 9813917
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"size": 223,
|
||||||
|
"weight": 892,
|
||||||
|
"sigops": 4,
|
||||||
|
"fee": 446,
|
||||||
|
"status": {
|
||||||
|
"confirmed": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,6 +96,18 @@ export const emitMempoolInfo = ({
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'txPosition': {
|
||||||
|
cy.readFile('cypress/fixtures/accelerated_position.json', 'ascii').then((fixture) => {
|
||||||
|
win.mockSocket.send(JSON.stringify(fixture));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'txPositionConfirmed': {
|
||||||
|
cy.readFile('cypress/fixtures/accelerated_position_confirmed.json', 'ascii').then((fixture) => {
|
||||||
|
win.mockSocket.send(JSON.stringify(fixture));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Routes, RouterModule } from '@angular/router';
|
|||||||
import { AppPreloadingStrategy } from './app.preloading-strategy'
|
import { AppPreloadingStrategy } from './app.preloading-strategy'
|
||||||
import { BlockViewComponent } from './components/block-view/block-view.component';
|
import { BlockViewComponent } from './components/block-view/block-view.component';
|
||||||
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
|
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
|
||||||
import { EightMempoolComponent } from './components/eight-mempool/eight-mempool.component';
|
|
||||||
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
|
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
|
||||||
import { ClockComponent } from './components/clock/clock.component';
|
import { ClockComponent } from './components/clock/clock.component';
|
||||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||||
@@ -206,10 +205,6 @@ let routes: Routes = [
|
|||||||
path: 'view/blocks',
|
path: 'view/blocks',
|
||||||
component: EightBlocksComponent,
|
component: EightBlocksComponent,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'view/mempool-blocks',
|
|
||||||
component: EightMempoolComponent,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'status',
|
path: 'status',
|
||||||
data: { networks: ['bitcoin', 'liquid'] },
|
data: { networks: ['bitcoin', 'liquid'] },
|
||||||
|
|||||||
@@ -681,9 +681,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
|
|
||||||
// WebGL shader attributes
|
// WebGL shader attributes
|
||||||
const attribs = {
|
const attribs = {
|
||||||
bounds: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
offset: { type: 'FLOAT', count: 2, pointer: null, offset: 0 },
|
||||||
posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
||||||
posY: { 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 },
|
colR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
||||||
colG: { 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 },
|
colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
||||||
@@ -706,9 +707,10 @@ varying lowp vec4 vColor;
|
|||||||
// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
|
// 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
|
// shader interpolates between start and end values at the given rate, from the given time
|
||||||
|
|
||||||
attribute vec4 bounds;
|
attribute vec2 offset;
|
||||||
attribute vec4 posX;
|
attribute vec4 posX;
|
||||||
attribute vec4 posY;
|
attribute vec4 posY;
|
||||||
|
attribute vec4 posR;
|
||||||
attribute vec4 colR;
|
attribute vec4 colR;
|
||||||
attribute vec4 colG;
|
attribute vec4 colG;
|
||||||
attribute vec4 colB;
|
attribute vec4 colB;
|
||||||
@@ -733,7 +735,10 @@ float interpolateAttribute(vec4 attr) {
|
|||||||
void main() {
|
void main() {
|
||||||
vec4 screenTransform = vec4(2.0 / screenSize.x, 2.0 / screenSize.y, -1.0, -1.0);
|
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);
|
// vec4 screenTransform = vec4(1.0 / screenSize.x, 1.0 / screenSize.y, -0.5, -0.5);
|
||||||
vec2 position = clamp(vec2(interpolateAttribute(posX), interpolateAttribute(posY)), bounds.xy, bounds.zw);
|
|
||||||
|
float radius = interpolateAttribute(posR);
|
||||||
|
vec2 position = vec2(interpolateAttribute(posX), interpolateAttribute(posY)) + (radius * offset);
|
||||||
|
|
||||||
gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
|
gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
|
||||||
|
|
||||||
float red = interpolateAttribute(colR);
|
float red = interpolateAttribute(colR);
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ export default class BlockScene {
|
|||||||
animationOffset: number;
|
animationOffset: number;
|
||||||
highlightingEnabled: boolean;
|
highlightingEnabled: boolean;
|
||||||
filterFlags: bigint | null = 0b00000100_00000000_00000000_00000000n;
|
filterFlags: bigint | null = 0b00000100_00000000_00000000_00000000n;
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
gridWidth: number;
|
gridWidth: number;
|
||||||
@@ -33,16 +31,14 @@ export default class BlockScene {
|
|||||||
animateUntil = 0;
|
animateUntil = 0;
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
|
|
||||||
constructor({ x = 0, y = 0, width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
|
constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
|
||||||
{ x?: number, y?: number, width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
|
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
|
||||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
|
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
|
||||||
) {
|
) {
|
||||||
this.init({ x, y,width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction });
|
this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction });
|
||||||
}
|
}
|
||||||
|
|
||||||
resize({ x = 0, y = 0, width = this.width, height = this.height, animate = true }: { x?: number, y?: number, width?: number, height?: number, animate: boolean }): void {
|
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.gridSize = this.width / this.gridWidth;
|
this.gridSize = this.width / this.gridWidth;
|
||||||
@@ -242,8 +238,8 @@ export default class BlockScene {
|
|||||||
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
|
this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
private init({ x, y, width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
|
private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, theme, highlighting, colorFunction }:
|
||||||
{ x: number, y: number, width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
|
{ width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number,
|
||||||
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
|
orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null }
|
||||||
): void {
|
): void {
|
||||||
this.animationDuration = animationDuration || this.animationDuration || 1000;
|
this.animationDuration = animationDuration || this.animationDuration || 1000;
|
||||||
@@ -268,7 +264,7 @@ export default class BlockScene {
|
|||||||
this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.02, 2);
|
this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.02, 2);
|
||||||
this.gridWidth = resolution;
|
this.gridWidth = resolution;
|
||||||
this.gridHeight = resolution;
|
this.gridHeight = resolution;
|
||||||
this.resize({ x, y, width, height, animate: true });
|
this.resize({ width, height, animate: true });
|
||||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
||||||
|
|
||||||
this.txs = {};
|
this.txs = {};
|
||||||
@@ -278,7 +274,7 @@ export default class BlockScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private applyTxUpdate(tx: TxView, update: ViewUpdateParams): void {
|
private applyTxUpdate(tx: TxView, update: ViewUpdateParams): void {
|
||||||
this.animateUntil = Math.max(this.animateUntil, tx.update(update, { minX: this.x, maxX: this.x + this.width, minY: this.y, maxY: this.y + this.height }));
|
this.animateUntil = Math.max(this.animateUntil, tx.update(update));
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateTxColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration?: number): void {
|
private updateTxColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration?: number): void {
|
||||||
@@ -394,7 +390,6 @@ export default class BlockScene {
|
|||||||
position: {
|
position: {
|
||||||
x: tx.screenPosition.x + (direction === 'right' ? this.width + this.animationOffset : (direction === 'left' ? -this.width - this.animationOffset : 0)),
|
x: tx.screenPosition.x + (direction === 'right' ? this.width + this.animationOffset : (direction === 'left' ? -this.width - this.animationOffset : 0)),
|
||||||
y: tx.screenPosition.y + (direction === 'up' ? this.height + this.animationOffset : (direction === 'down' ? -this.height - this.animationOffset : 0)),
|
y: tx.screenPosition.y + (direction === 'up' ? this.height + this.animationOffset : (direction === 'down' ? -this.height - this.animationOffset : 0)),
|
||||||
s: tx.screenPosition.s
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
duration: this.animationDuration,
|
duration: this.animationDuration,
|
||||||
@@ -454,18 +449,18 @@ export default class BlockScene {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
x: this.x + x + this.unitPadding - (slotSize / 2),
|
x: x + this.unitPadding - (slotSize / 2),
|
||||||
y: this.y + y + this.unitPadding - (slotSize / 2),
|
y: y + this.unitPadding - (slotSize / 2),
|
||||||
s: squareSize
|
s: squareSize
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return { x: this.x, y: this.y, s: 0 };
|
return { x: 0, y: 0, s: 0 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private screenToGrid(position: Position): Position {
|
private screenToGrid(position: Position): Position {
|
||||||
let x = position.x - this.x;
|
let x = position.x;
|
||||||
let y = this.height - (position.y - this.y);
|
let y = this.height - position.y;
|
||||||
let t;
|
let t;
|
||||||
|
|
||||||
switch (this.orientation) {
|
switch (this.orientation) {
|
||||||
|
|||||||
@@ -2,13 +2,12 @@ import { FastVertexArray } from './fast-vertex-array';
|
|||||||
import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from './sprite-types';
|
import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from './sprite-types';
|
||||||
|
|
||||||
const attribKeys = ['a', 'b', 't', 'v'];
|
const attribKeys = ['a', 'b', 't', 'v'];
|
||||||
const updateKeys = ['x', 'y', 'r', 'g', 'b', 'a'];
|
const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];
|
||||||
const attributeKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];
|
|
||||||
|
|
||||||
export default class TxSprite {
|
export default class TxSprite {
|
||||||
static vertexSize = 28;
|
static vertexSize = 30;
|
||||||
static vertexCount = 6;
|
static vertexCount = 6;
|
||||||
static dataSize: number = (28 * 6);
|
static dataSize: number = (30 * 6);
|
||||||
|
|
||||||
vertexArray: FastVertexArray;
|
vertexArray: FastVertexArray;
|
||||||
vertexPointer: number;
|
vertexPointer: number;
|
||||||
@@ -17,26 +16,15 @@ export default class TxSprite {
|
|||||||
attributes: Attributes;
|
attributes: Attributes;
|
||||||
tempAttributes: OptionalAttributes;
|
tempAttributes: OptionalAttributes;
|
||||||
|
|
||||||
minX: number;
|
|
||||||
maxX: number;
|
|
||||||
minY: number;
|
|
||||||
maxY: number;
|
|
||||||
|
|
||||||
|
constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray) {
|
||||||
constructor(params: SpriteUpdateParams, vertexArray: FastVertexArray, minX: number, maxX: number, minY: number, maxY: number) {
|
|
||||||
const offsetTime = params.start;
|
const offsetTime = params.start;
|
||||||
this.vertexArray = vertexArray;
|
this.vertexArray = vertexArray;
|
||||||
this.vertexData = Array(TxSprite.dataSize).fill(0);
|
this.vertexData = Array(VI.length).fill(0);
|
||||||
|
|
||||||
this.updateMap = {
|
this.updateMap = {
|
||||||
x: 0, y: 0, s: 0, r: 0, g: 0, b: 0, a: 0
|
x: 0, y: 0, s: 0, r: 0, g: 0, b: 0, a: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
this.minX = minX;
|
|
||||||
this.maxX = maxX;
|
|
||||||
this.minY = minY;
|
|
||||||
this.maxY = maxY;
|
|
||||||
|
|
||||||
this.attributes = {
|
this.attributes = {
|
||||||
x: { a: params.x, b: params.x, t: offsetTime, v: 0, d: 0 },
|
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 },
|
y: { a: params.y, b: params.y, t: offsetTime, v: 0, d: 0 },
|
||||||
@@ -89,24 +77,11 @@ export default class TxSprite {
|
|||||||
minDuration: minimum remaining transition duration when adjust = true
|
minDuration: minimum remaining transition duration when adjust = true
|
||||||
temp: if true, this update is only temporary (can be reversed with 'resume')
|
temp: if true, this update is only temporary (can be reversed with 'resume')
|
||||||
*/
|
*/
|
||||||
update(params: SpriteUpdateParams, minX?: number, maxX?: number, minY?: number, maxY?: number): void {
|
update(params: SpriteUpdateParams): void {
|
||||||
const offsetTime = params.start || performance.now();
|
const offsetTime = params.start || performance.now();
|
||||||
const v = params.duration > 0 ? (1 / params.duration) : 0;
|
const v = params.duration > 0 ? (1 / params.duration) : 0;
|
||||||
|
|
||||||
if (minX != null) {
|
updateKeys.forEach(key => {
|
||||||
this.minX = minX;
|
|
||||||
}
|
|
||||||
if (maxX != null) {
|
|
||||||
this.maxX = maxX;
|
|
||||||
}
|
|
||||||
if (minY != null) {
|
|
||||||
this.minY = minY;
|
|
||||||
}
|
|
||||||
if (maxY != null) {
|
|
||||||
this.maxY = maxY;
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeKeys.forEach(key => {
|
|
||||||
this.updateMap[key] = params[key];
|
this.updateMap[key] = params[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -164,32 +139,18 @@ export default class TxSprite {
|
|||||||
...this.tempAttributes
|
...this.tempAttributes
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const size = attributes.s;
|
||||||
|
|
||||||
// update vertex data in place
|
// update vertex data in place
|
||||||
// ugly, but avoids overhead of allocating large temporary arrays
|
// ugly, but avoids overhead of allocating large temporary arrays
|
||||||
const vertexStride = VI.length + 4;
|
const vertexStride = VI.length + 2;
|
||||||
for (let vertex = 0; vertex < 6; vertex++) {
|
for (let vertex = 0; vertex < 6; vertex++) {
|
||||||
this.vertexData[vertex * vertexStride] = this.minX;
|
this.vertexData[vertex * vertexStride] = vertexOffsetFactors[vertex][0];
|
||||||
this.vertexData[(vertex * vertexStride) + 1] = this.minY;
|
this.vertexData[(vertex * vertexStride) + 1] = vertexOffsetFactors[vertex][1];
|
||||||
this.vertexData[(vertex * vertexStride) + 2] = this.maxX;
|
for (let step = 0; step < VI.length; step++) {
|
||||||
this.vertexData[(vertex * vertexStride) + 3] = this.maxY;
|
|
||||||
|
|
||||||
// x
|
|
||||||
this.vertexData[(vertex * vertexStride) + 4] = attributes[VI[0].a][VI[0].f] + (vertexOffsetFactors[vertex][0] * attributes.s.a);
|
|
||||||
this.vertexData[(vertex * vertexStride) + 5] = attributes[VI[1].a][VI[1].f] + (vertexOffsetFactors[vertex][0] * attributes.s.b);
|
|
||||||
this.vertexData[(vertex * vertexStride) + 6] = attributes[VI[2].a][VI[2].f];
|
|
||||||
this.vertexData[(vertex * vertexStride) + 7] = attributes[VI[3].a][VI[3].f];
|
|
||||||
|
|
||||||
// y
|
|
||||||
this.vertexData[(vertex * vertexStride) + 8] = attributes[VI[4].a][VI[4].f] + (vertexOffsetFactors[vertex][1] * attributes.s.a);
|
|
||||||
this.vertexData[(vertex * vertexStride) + 9] = attributes[VI[5].a][VI[5].f] + (vertexOffsetFactors[vertex][1] * attributes.s.b);
|
|
||||||
this.vertexData[(vertex * vertexStride) + 10] = attributes[VI[6].a][VI[6].f];
|
|
||||||
this.vertexData[(vertex * vertexStride) + 11] = attributes[VI[7].a][VI[7].f];
|
|
||||||
|
|
||||||
for (let step = 8; step < VI.length; step++) {
|
|
||||||
// components of each field in the vertex array are defined by an entry in VI:
|
// 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
|
// VI[i].a is the attribute, VI[i].f is the inner field, VI[i].offA and VI[i].offB are offset factors
|
||||||
this.vertexData[(vertex * vertexStride) + step + 4] = attributes[VI[step].a][VI[step].f];
|
this.vertexData[(vertex * vertexStride) + step + 2] = attributes[VI[step].a][VI[step].f];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default class TxView implements TransactionStripped {
|
|||||||
|
|
||||||
returns minimum transition end time
|
returns minimum transition end time
|
||||||
*/
|
*/
|
||||||
update(params: ViewUpdateParams, { minX, maxX, minY, maxY }: { minX: number, maxX: number, minY: number, maxY: number }): number {
|
update(params: ViewUpdateParams): number {
|
||||||
if (params.jitter) {
|
if (params.jitter) {
|
||||||
params.delay += (Math.random() * params.jitter);
|
params.delay += (Math.random() * params.jitter);
|
||||||
}
|
}
|
||||||
@@ -115,35 +115,21 @@ export default class TxView implements TransactionStripped {
|
|||||||
this.initialised = true;
|
this.initialised = true;
|
||||||
this.sprite = new TxSprite(
|
this.sprite = new TxSprite(
|
||||||
toSpriteUpdate(params),
|
toSpriteUpdate(params),
|
||||||
this.vertexArray,
|
this.vertexArray
|
||||||
minX,
|
|
||||||
maxX,
|
|
||||||
minY,
|
|
||||||
maxY
|
|
||||||
);
|
);
|
||||||
// apply any pending hover event
|
// apply any pending hover event
|
||||||
if (this.hover) {
|
if (this.hover) {
|
||||||
params.duration = Math.max(params.duration, hoverTransitionTime);
|
params.duration = Math.max(params.duration, hoverTransitionTime);
|
||||||
this.sprite.update(
|
this.sprite.update({
|
||||||
{
|
...this.hoverColor,
|
||||||
...this.hoverColor,
|
duration: hoverTransitionTime,
|
||||||
duration: hoverTransitionTime,
|
adjust: false,
|
||||||
adjust: false,
|
temp: true
|
||||||
temp: true
|
});
|
||||||
},
|
|
||||||
minX,
|
|
||||||
maxX,
|
|
||||||
minY,
|
|
||||||
maxY
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.sprite.update(
|
this.sprite.update(
|
||||||
toSpriteUpdate(params),
|
toSpriteUpdate(params)
|
||||||
minX,
|
|
||||||
maxX,
|
|
||||||
minY,
|
|
||||||
maxY
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
|
|
||||||
<div class="block-overview-graph">
|
|
||||||
<canvas *browserOnly class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
|
|
||||||
@if (!disableSpinner) {
|
|
||||||
<div class="loader-wrapper" [class.hidden]="!isLoading && !unavailable">
|
|
||||||
<div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div>
|
|
||||||
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<app-block-overview-tooltip
|
|
||||||
[tx]="selectedTx || hoverTx"
|
|
||||||
[cursorPosition]="tooltipPosition"
|
|
||||||
[clickable]="!!selectedTx"
|
|
||||||
[auditEnabled]="auditHighlighting"
|
|
||||||
[blockConversion]="blockConversion"
|
|
||||||
[filterFlags]="activeFilterFlags"
|
|
||||||
[filterMode]="filterMode"
|
|
||||||
[relativeTime]="relativeTime"
|
|
||||||
></app-block-overview-tooltip>
|
|
||||||
<app-block-filters *ngIf="webGlEnabled && showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
|
|
||||||
<div *ngIf="!webGlEnabled" class="placeholder">
|
|
||||||
<span i18n="webgl-disabled">Your browser does not support this feature.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
.block-overview-graph {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: var(--stat-box-bg);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
grid-column: 1/-1;
|
|
||||||
|
|
||||||
.placeholder {
|
|
||||||
display: flex;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.graph-alignment {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-align {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, 75px);
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-overview-canvas {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&.clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.loader-wrapper {
|
|
||||||
position: absolute;
|
|
||||||
background: #181b2d7f;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
transition: opacity 500ms 500ms;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,647 +0,0 @@
|
|||||||
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core';
|
|
||||||
import { TransactionStripped } from '../../interfaces/node-api.interface';
|
|
||||||
import { FastVertexArray } from '../block-overview-graph/fast-vertex-array';
|
|
||||||
import BlockScene from '../block-overview-graph/block-scene';
|
|
||||||
import TxSprite from '../block-overview-graph/tx-sprite';
|
|
||||||
import TxView from '../block-overview-graph/tx-view';
|
|
||||||
import { Color, Position } from '../block-overview-graph/sprite-types';
|
|
||||||
import { Price } from '../../services/price.service';
|
|
||||||
import { StateService } from '../../services/state.service';
|
|
||||||
import { ThemeService } from '../../services/theme.service';
|
|
||||||
import { Subscription } from 'rxjs';
|
|
||||||
import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction, contrastColorFunction, contrastAuditColors, contrastColors } from '../block-overview-graph/utils';
|
|
||||||
import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils';
|
|
||||||
import { detectWebGL } from '../../shared/graphs.utils';
|
|
||||||
|
|
||||||
const unmatchedOpacity = 0.2;
|
|
||||||
const unmatchedAuditColors = {
|
|
||||||
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
|
|
||||||
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
|
|
||||||
added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
|
|
||||||
added_prioritized: setOpacity(defaultAuditColors.added_prioritized, unmatchedOpacity),
|
|
||||||
prioritized: setOpacity(defaultAuditColors.prioritized, unmatchedOpacity),
|
|
||||||
accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
|
|
||||||
};
|
|
||||||
const unmatchedContrastAuditColors = {
|
|
||||||
censored: setOpacity(contrastAuditColors.censored, unmatchedOpacity),
|
|
||||||
missing: setOpacity(contrastAuditColors.missing, unmatchedOpacity),
|
|
||||||
added: setOpacity(contrastAuditColors.added, unmatchedOpacity),
|
|
||||||
added_prioritized: setOpacity(contrastAuditColors.added_prioritized, unmatchedOpacity),
|
|
||||||
prioritized: setOpacity(contrastAuditColors.prioritized, unmatchedOpacity),
|
|
||||||
accelerated: setOpacity(contrastAuditColors.accelerated, unmatchedOpacity),
|
|
||||||
};
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-block-overview-multi',
|
|
||||||
templateUrl: './block-overview-multi.component.html',
|
|
||||||
styleUrls: ['./block-overview-multi.component.scss'],
|
|
||||||
})
|
|
||||||
export class BlockOverviewMultiComponent implements AfterViewInit, OnDestroy, OnChanges {
|
|
||||||
@Input() isLoading: boolean;
|
|
||||||
@Input() resolution: number;
|
|
||||||
@Input() numBlocks: number;
|
|
||||||
@Input() padding: number = 0;
|
|
||||||
@Input() blockWidth: number = 360;
|
|
||||||
@Input() autofit: boolean = false;
|
|
||||||
@Input() blockLimit: number;
|
|
||||||
@Input() orientation = 'left';
|
|
||||||
@Input() flip = true;
|
|
||||||
@Input() animationDuration: number = 1000;
|
|
||||||
@Input() animationOffset: number | null = null;
|
|
||||||
@Input() disableSpinner = false;
|
|
||||||
@Input() mirrorTxid: string | void;
|
|
||||||
@Input() unavailable: boolean = false;
|
|
||||||
@Input() auditHighlighting: boolean = false;
|
|
||||||
@Input() showFilters: boolean = false;
|
|
||||||
@Input() excludeFilters: string[] = [];
|
|
||||||
@Input() filterFlags: bigint | null = null;
|
|
||||||
@Input() filterMode: FilterMode = 'and';
|
|
||||||
@Input() gradientMode: 'fee' | 'age' = 'fee';
|
|
||||||
@Input() relativeTime: number | null;
|
|
||||||
@Input() blockConversion: Price;
|
|
||||||
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
|
|
||||||
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
|
|
||||||
@Output() txHoverEvent = new EventEmitter<string>();
|
|
||||||
@Output() readyEvent = new EventEmitter();
|
|
||||||
|
|
||||||
@ViewChild('blockCanvas')
|
|
||||||
canvas: ElementRef<HTMLCanvasElement>;
|
|
||||||
themeChangedSubscription: Subscription;
|
|
||||||
|
|
||||||
gl: WebGLRenderingContext;
|
|
||||||
animationFrameRequest: number;
|
|
||||||
animationHeartBeat: number;
|
|
||||||
displayWidth: number;
|
|
||||||
displayHeight: number;
|
|
||||||
displayBlockWidth: number;
|
|
||||||
displayPadding: number;
|
|
||||||
cssWidth: number;
|
|
||||||
cssHeight: number;
|
|
||||||
shaderProgram: WebGLProgram;
|
|
||||||
vertexArray: FastVertexArray;
|
|
||||||
running: boolean;
|
|
||||||
scenes: BlockScene[] = [];
|
|
||||||
hoverTx: TxView | void;
|
|
||||||
selectedTx: TxView | void;
|
|
||||||
highlightTx: TxView | void;
|
|
||||||
mirrorTx: TxView | void;
|
|
||||||
tooltipPosition: Position;
|
|
||||||
|
|
||||||
readyNextFrame = false;
|
|
||||||
lastUpdate: number = 0;
|
|
||||||
pendingUpdates: {
|
|
||||||
count: number,
|
|
||||||
add: { [txid: string]: TransactionStripped },
|
|
||||||
remove: { [txid: string]: string },
|
|
||||||
change: { [txid: string]: { txid: string, rate: number | undefined, acc: boolean | undefined } },
|
|
||||||
direction?: string,
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
searchText: string;
|
|
||||||
searchSubscription: Subscription;
|
|
||||||
filtersAvailable: boolean = true;
|
|
||||||
activeFilterFlags: bigint | null = null;
|
|
||||||
|
|
||||||
webGlEnabled = true;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly ngZone: NgZone,
|
|
||||||
readonly elRef: ElementRef,
|
|
||||||
public stateService: StateService,
|
|
||||||
private themeService: ThemeService,
|
|
||||||
) {
|
|
||||||
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
|
|
||||||
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
|
||||||
if (this.canvas) {
|
|
||||||
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
|
|
||||||
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
|
|
||||||
this.gl = this.canvas.nativeElement.getContext('webgl');
|
|
||||||
this.initScenes();
|
|
||||||
|
|
||||||
if (this.gl) {
|
|
||||||
this.initCanvas();
|
|
||||||
this.resizeCanvas();
|
|
||||||
this.themeChangedSubscription = this.themeService.themeChanged$.subscribe(() => {
|
|
||||||
for (const scene of this.scenes) {
|
|
||||||
scene.setColorFunction(this.getColorFunction());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initScenes(): void {
|
|
||||||
for (const scene of this.scenes) {
|
|
||||||
if (scene) {
|
|
||||||
scene.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.scenes = [];
|
|
||||||
this.pendingUpdates = [];
|
|
||||||
for (let i = 0; i < this.numBlocks; i++) {
|
|
||||||
this.scenes.push(null);
|
|
||||||
this.pendingUpdates.push({
|
|
||||||
count: 0,
|
|
||||||
add: {},
|
|
||||||
remove: {},
|
|
||||||
change: {},
|
|
||||||
direction: 'left',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.resizeCanvas();
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges(changes): void {
|
|
||||||
if (changes.numBlocks) {
|
|
||||||
this.initScenes();
|
|
||||||
}
|
|
||||||
if (changes.orientation || changes.flip) {
|
|
||||||
for (const scene of this.scenes) {
|
|
||||||
scene?.setOrientation(this.orientation, this.flip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (changes.auditHighlighting) {
|
|
||||||
this.setHighlightingEnabled(this.auditHighlighting);
|
|
||||||
}
|
|
||||||
if (changes.overrideColor) {
|
|
||||||
for (const scene of this.scenes) {
|
|
||||||
scene?.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((changes.filterFlags || changes.showFilters || changes.filterMode || changes.gradientMode)) {
|
|
||||||
this.setFilterFlags();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilterFlags(goggle?: ActiveFilter): void {
|
|
||||||
this.filterMode = goggle?.mode || this.filterMode;
|
|
||||||
this.gradientMode = goggle?.gradient || this.gradientMode;
|
|
||||||
this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
|
|
||||||
for (const scene of this.scenes) {
|
|
||||||
if (this.activeFilterFlags != null && this.filtersAvailable) {
|
|
||||||
scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode));
|
|
||||||
} else {
|
|
||||||
scene.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
if (this.animationFrameRequest) {
|
|
||||||
cancelAnimationFrame(this.animationFrameRequest);
|
|
||||||
clearTimeout(this.animationHeartBeat);
|
|
||||||
}
|
|
||||||
if (this.canvas) {
|
|
||||||
this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
|
|
||||||
this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
|
|
||||||
this.themeChangedSubscription?.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(block: number, direction): void {
|
|
||||||
this.exit(block, direction);
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy(block: number): void {
|
|
||||||
if (this.scenes[block]) {
|
|
||||||
this.scenes[block].destroy();
|
|
||||||
this.clearUpdateQueue(block);
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize the scene without any entry transition
|
|
||||||
setup(block: number, transactions: TransactionStripped[], sort: boolean = false): void {
|
|
||||||
const filtersAvailable = transactions.reduce((flagSet, tx) => flagSet || tx.flags > 0, false);
|
|
||||||
if (filtersAvailable !== this.filtersAvailable) {
|
|
||||||
this.setFilterFlags();
|
|
||||||
}
|
|
||||||
this.filtersAvailable = filtersAvailable;
|
|
||||||
if (this.scenes[block]) {
|
|
||||||
this.clearUpdateQueue(block);
|
|
||||||
this.scenes[block].setup(transactions, sort);
|
|
||||||
this.readyNextFrame = true;
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enter(block: number, transactions: TransactionStripped[], direction: string): void {
|
|
||||||
if (this.scenes[block]) {
|
|
||||||
this.clearUpdateQueue(block);
|
|
||||||
this.scenes[block].enter(transactions, direction);
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit(block: number, direction: string): void {
|
|
||||||
if (this.scenes[block]) {
|
|
||||||
this.clearUpdateQueue(block);
|
|
||||||
this.scenes[block].exit(direction);
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
replace(block: number, transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void {
|
|
||||||
if (this.scenes[block]) {
|
|
||||||
this.clearUpdateQueue(block);
|
|
||||||
this.scenes[block].replace(transactions || [], direction, sort, startTime);
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// collates deferred updates into a set of consistent pending changes
|
|
||||||
queueUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
|
|
||||||
for (const tx of add) {
|
|
||||||
this.pendingUpdates[block].add[tx.txid] = tx;
|
|
||||||
delete this.pendingUpdates[block].remove[tx.txid];
|
|
||||||
delete this.pendingUpdates[block].change[tx.txid];
|
|
||||||
}
|
|
||||||
for (const txid of remove) {
|
|
||||||
delete this.pendingUpdates[block].add[txid];
|
|
||||||
this.pendingUpdates[block].remove[txid] = txid;
|
|
||||||
delete this.pendingUpdates[block].change[txid];
|
|
||||||
}
|
|
||||||
for (const tx of change) {
|
|
||||||
if (this.pendingUpdates[block].add[tx.txid]) {
|
|
||||||
this.pendingUpdates[block].add[tx.txid].rate = tx.rate;
|
|
||||||
this.pendingUpdates[block].add[tx.txid].acc = tx.acc;
|
|
||||||
} else {
|
|
||||||
this.pendingUpdates[block].change[tx.txid] = tx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.pendingUpdates[block].direction = direction;
|
|
||||||
this.pendingUpdates[block].count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
deferredUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void {
|
|
||||||
this.queueUpdate(block, add, remove, change, direction);
|
|
||||||
this.applyQueuedUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
applyQueuedUpdates(): void {
|
|
||||||
for (const [index, pendingUpdate] of this.pendingUpdates.entries()) {
|
|
||||||
if (pendingUpdate.count && performance.now() > (this.lastUpdate + this.animationDuration)) {
|
|
||||||
this.applyUpdate(index, Object.values(pendingUpdate.add), Object.values(pendingUpdate.remove), Object.values(pendingUpdate.change), pendingUpdate.direction);
|
|
||||||
this.clearUpdateQueue(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearUpdateQueue(block: number): void {
|
|
||||||
this.pendingUpdates[block] = {
|
|
||||||
count: 0,
|
|
||||||
add: {},
|
|
||||||
remove: {},
|
|
||||||
change: {},
|
|
||||||
};
|
|
||||||
this.lastUpdate = performance.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
update(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
|
||||||
// merge any pending changes into this update
|
|
||||||
this.queueUpdate(block, add, remove, change, direction);
|
|
||||||
this.applyUpdate(block,Object.values(this.pendingUpdates[block].add), Object.values(this.pendingUpdates[block].remove), Object.values(this.pendingUpdates[block].change), direction, resetLayout);
|
|
||||||
this.clearUpdateQueue(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
applyUpdate(block: number, add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
|
||||||
if (this.scenes[block]) {
|
|
||||||
add = add.filter(tx => !this.scenes[block].txs[tx.txid]);
|
|
||||||
remove = remove.filter(txid => this.scenes[block].txs[txid]);
|
|
||||||
change = change.filter(tx => this.scenes[block].txs[tx.txid]);
|
|
||||||
|
|
||||||
if (this.gradientMode === 'age') {
|
|
||||||
this.scenes[block].updateAllColors();
|
|
||||||
}
|
|
||||||
this.scenes[block].update(add, remove, change, direction, resetLayout);
|
|
||||||
this.start();
|
|
||||||
this.lastUpdate = performance.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initCanvas(): void {
|
|
||||||
if (!this.canvas || !this.gl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
|
||||||
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
|
||||||
|
|
||||||
const shaderSet = [
|
|
||||||
{
|
|
||||||
type: this.gl.VERTEX_SHADER,
|
|
||||||
src: vertShaderSrc
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: this.gl.FRAGMENT_SHADER,
|
|
||||||
src: fragShaderSrc
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
this.shaderProgram = this.buildShaderProgram(shaderSet);
|
|
||||||
|
|
||||||
this.gl.useProgram(this.shaderProgram);
|
|
||||||
|
|
||||||
// Set up alpha blending
|
|
||||||
this.gl.enable(this.gl.BLEND);
|
|
||||||
this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
|
|
||||||
|
|
||||||
const glBuffer = this.gl.createBuffer();
|
|
||||||
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, glBuffer);
|
|
||||||
|
|
||||||
/* SET UP SHADER ATTRIBUTES */
|
|
||||||
Object.keys(attribs).forEach((key, i) => {
|
|
||||||
attribs[key].pointer = this.gl.getAttribLocation(this.shaderProgram, key);
|
|
||||||
this.gl.enableVertexAttribArray(attribs[key].pointer);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContextLost(event): void {
|
|
||||||
event.preventDefault();
|
|
||||||
cancelAnimationFrame(this.animationFrameRequest);
|
|
||||||
this.animationFrameRequest = null;
|
|
||||||
this.running = false;
|
|
||||||
this.gl = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleContextRestored(event): void {
|
|
||||||
if (this.canvas?.nativeElement) {
|
|
||||||
this.gl = this.canvas.nativeElement.getContext('webgl');
|
|
||||||
if (this.gl) {
|
|
||||||
this.initCanvas();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@HostListener('window:resize', ['$event'])
|
|
||||||
resizeCanvas(): void {
|
|
||||||
if (this.canvas) {
|
|
||||||
this.cssWidth = this.canvas.nativeElement.offsetParent.clientWidth;
|
|
||||||
this.cssHeight = this.canvas.nativeElement.offsetParent.clientHeight;
|
|
||||||
this.displayWidth = window.devicePixelRatio * this.cssWidth;
|
|
||||||
this.displayHeight = window.devicePixelRatio * this.cssHeight;
|
|
||||||
this.displayBlockWidth = window.devicePixelRatio * this.blockWidth;
|
|
||||||
this.displayPadding = window.devicePixelRatio * this.padding;
|
|
||||||
this.canvas.nativeElement.width = this.displayWidth;
|
|
||||||
this.canvas.nativeElement.height = this.displayHeight;
|
|
||||||
if (this.gl) {
|
|
||||||
this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
|
|
||||||
}
|
|
||||||
for (let i = 0; i < this.scenes.length; i++) {
|
|
||||||
const blocksPerRow = Math.floor(this.displayWidth / (this.displayBlockWidth + (this.displayPadding * 2)));
|
|
||||||
const x = this.displayPadding + ((i % blocksPerRow) * (this.displayBlockWidth + (this.displayPadding * 2)));
|
|
||||||
const row = Math.floor(i / blocksPerRow);
|
|
||||||
const y = this.displayPadding + this.displayHeight - ((row + 1) * (this.displayBlockWidth + (this.displayPadding * 2)));
|
|
||||||
if (this.scenes[i]) {
|
|
||||||
this.scenes[i].resize({ x, y, width: this.displayBlockWidth, height: this.displayBlockWidth, animate: false });
|
|
||||||
this.start();
|
|
||||||
} else {
|
|
||||||
this.scenes[i] = new BlockScene({ x, y, width: this.displayBlockWidth, height: this.displayBlockWidth, resolution: this.resolution,
|
|
||||||
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, theme: this.themeService,
|
|
||||||
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
|
|
||||||
colorFunction: this.getColorFunction() });
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileShader(src, type): WebGLShader {
|
|
||||||
if (!this.gl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const shader = this.gl.createShader(type);
|
|
||||||
|
|
||||||
this.gl.shaderSource(shader, src);
|
|
||||||
this.gl.compileShader(shader);
|
|
||||||
|
|
||||||
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
|
||||||
console.log(`Error compiling ${type === this.gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:`);
|
|
||||||
console.log(this.gl.getShaderInfoLog(shader));
|
|
||||||
}
|
|
||||||
return shader;
|
|
||||||
}
|
|
||||||
|
|
||||||
buildShaderProgram(shaderInfo): WebGLProgram {
|
|
||||||
if (!this.gl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const program = this.gl.createProgram();
|
|
||||||
|
|
||||||
shaderInfo.forEach((desc) => {
|
|
||||||
const shader = this.compileShader(desc.src, desc.type);
|
|
||||||
if (shader) {
|
|
||||||
this.gl.attachShader(program, shader);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.gl.linkProgram(program);
|
|
||||||
|
|
||||||
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
|
||||||
console.log('Error linking shader program:');
|
|
||||||
console.log(this.gl.getProgramInfoLog(program));
|
|
||||||
}
|
|
||||||
|
|
||||||
return program;
|
|
||||||
}
|
|
||||||
|
|
||||||
start(): void {
|
|
||||||
this.running = true;
|
|
||||||
this.ngZone.runOutsideAngular(() => this.doRun());
|
|
||||||
}
|
|
||||||
|
|
||||||
doRun(): void {
|
|
||||||
if (this.animationFrameRequest) {
|
|
||||||
cancelAnimationFrame(this.animationFrameRequest);
|
|
||||||
}
|
|
||||||
this.animationFrameRequest = requestAnimationFrame(() => this.run());
|
|
||||||
}
|
|
||||||
|
|
||||||
run(now?: DOMHighResTimeStamp): void {
|
|
||||||
if (!now) {
|
|
||||||
now = performance.now();
|
|
||||||
}
|
|
||||||
this.applyQueuedUpdates();
|
|
||||||
// skip re-render if there's no change to the scene
|
|
||||||
if (this.scenes.length && this.gl) {
|
|
||||||
/* SET UP SHADER UNIFORMS */
|
|
||||||
// screen dimensions
|
|
||||||
this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
|
|
||||||
// frame timestamp
|
|
||||||
this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, 'now'), now);
|
|
||||||
|
|
||||||
if (this.vertexArray.dirty) {
|
|
||||||
/* SET UP SHADER ATTRIBUTES */
|
|
||||||
Object.keys(attribs).forEach((key, i) => {
|
|
||||||
this.gl.vertexAttribPointer(attribs[key].pointer,
|
|
||||||
attribs[key].count, // number of primitives in this attribute
|
|
||||||
this.gl[attribs[key].type], // type of primitive in this attribute (e.g. gl.FLOAT)
|
|
||||||
false, // never normalised
|
|
||||||
stride, // distance between values of the same attribute
|
|
||||||
attribs[key].offset); // offset of the first value
|
|
||||||
});
|
|
||||||
|
|
||||||
const pointArray = this.vertexArray.getVertexData();
|
|
||||||
|
|
||||||
if (pointArray.length) {
|
|
||||||
this.gl.bufferData(this.gl.ARRAY_BUFFER, pointArray, this.gl.DYNAMIC_DRAW);
|
|
||||||
this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
|
|
||||||
}
|
|
||||||
this.vertexArray.dirty = false;
|
|
||||||
} else {
|
|
||||||
const pointArray = this.vertexArray.getVertexData();
|
|
||||||
if (pointArray.length) {
|
|
||||||
this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.readyNextFrame) {
|
|
||||||
this.readyNextFrame = false;
|
|
||||||
this.readyEvent.emit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* LOOP */
|
|
||||||
if (this.running && this.scenes.length && now <= (this.scenes.reduce((max, scene) => scene.animateUntil > max ? scene.animateUntil : max, 0) + 500)) {
|
|
||||||
this.doRun();
|
|
||||||
} else {
|
|
||||||
if (this.animationHeartBeat) {
|
|
||||||
clearTimeout(this.animationHeartBeat);
|
|
||||||
}
|
|
||||||
this.animationHeartBeat = window.setTimeout(() => {
|
|
||||||
this.start();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setHighlightingEnabled(enabled: boolean): void {
|
|
||||||
for (const scene of this.scenes) {
|
|
||||||
scene.setHighlighting(enabled);
|
|
||||||
}
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
getColorFunction(): ((tx: TxView) => Color) {
|
|
||||||
if (this.overrideColors) {
|
|
||||||
return this.overrideColors;
|
|
||||||
} else if (this.filterFlags) {
|
|
||||||
return this.getFilterColorFunction(this.filterFlags, this.gradientMode);
|
|
||||||
} else if (this.activeFilterFlags) {
|
|
||||||
return this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode);
|
|
||||||
} else {
|
|
||||||
return this.getFilterColorFunction(0n, this.gradientMode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) {
|
|
||||||
return (tx: TxView) => {
|
|
||||||
if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
|
|
||||||
if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
|
|
||||||
return (gradient === 'age') ? ageColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)) : defaultColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000));
|
|
||||||
} else {
|
|
||||||
return (gradient === 'age') ? ageColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)) : contrastColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
|
|
||||||
return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : defaultColorFunction(
|
|
||||||
tx,
|
|
||||||
defaultColors.unmatchedfee,
|
|
||||||
unmatchedAuditColors,
|
|
||||||
this.relativeTime || (Date.now() / 1000)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : contrastColorFunction(
|
|
||||||
tx,
|
|
||||||
contrastColors.unmatchedfee,
|
|
||||||
unmatchedContrastAuditColors,
|
|
||||||
this.relativeTime || (Date.now() / 1000)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebGL shader attributes
|
|
||||||
const attribs = {
|
|
||||||
bounds: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
|
||||||
posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
|
||||||
posY: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
|
||||||
colR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
|
||||||
colG: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
|
||||||
colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
|
|
||||||
colA: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }
|
|
||||||
};
|
|
||||||
// Calculate the number of bytes per vertex based on specified attributes
|
|
||||||
const stride = Object.values(attribs).reduce((total, attrib) => {
|
|
||||||
return total + (attrib.count * 4);
|
|
||||||
}, 0);
|
|
||||||
// Calculate vertex attribute offsets
|
|
||||||
for (let i = 0, offset = 0; i < Object.keys(attribs).length; i++) {
|
|
||||||
const attrib = Object.values(attribs)[i];
|
|
||||||
attrib.offset = offset;
|
|
||||||
offset += (attrib.count * 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
const vertShaderSrc = `
|
|
||||||
varying lowp vec4 vColor;
|
|
||||||
|
|
||||||
// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
|
|
||||||
// shader interpolates between start and end values at the given rate, from the given time
|
|
||||||
|
|
||||||
attribute vec4 bounds;
|
|
||||||
attribute vec4 posX;
|
|
||||||
attribute vec4 posY;
|
|
||||||
attribute vec4 colR;
|
|
||||||
attribute vec4 colG;
|
|
||||||
attribute vec4 colB;
|
|
||||||
attribute vec4 colA;
|
|
||||||
|
|
||||||
uniform vec2 screenSize;
|
|
||||||
uniform float now;
|
|
||||||
|
|
||||||
float smootherstep(float x) {
|
|
||||||
x = clamp(x, 0.0, 1.0);
|
|
||||||
float ix = 1.0 - x;
|
|
||||||
x = x * x;
|
|
||||||
return x / (x + ix * ix);
|
|
||||||
}
|
|
||||||
|
|
||||||
float interpolateAttribute(vec4 attr) {
|
|
||||||
float d = (now - attr.z) * attr.w;
|
|
||||||
float delta = smootherstep(d);
|
|
||||||
return mix(attr.x, attr.y, delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec4 screenTransform = vec4(2.0 / screenSize.x, 2.0 / screenSize.y, -1.0, -1.0);
|
|
||||||
// vec4 screenTransform = vec4(1.0 / screenSize.x, 1.0 / screenSize.y, -0.5, -0.5);
|
|
||||||
vec2 position = clamp(vec2(interpolateAttribute(posX), interpolateAttribute(posY)), bounds.xy, bounds.zw);
|
|
||||||
gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
|
|
||||||
|
|
||||||
float red = interpolateAttribute(colR);
|
|
||||||
float green = interpolateAttribute(colG);
|
|
||||||
float blue = interpolateAttribute(colB);
|
|
||||||
float alpha = interpolateAttribute(colA);
|
|
||||||
|
|
||||||
vColor = vec4(red, green, blue, alpha);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const fragShaderSrc = `
|
|
||||||
varying lowp vec4 vColor;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
gl_FragColor = vColor;
|
|
||||||
// premultiply alpha
|
|
||||||
gl_FragColor.rgb *= gl_FragColor.a;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
@@ -1,25 +1,23 @@
|
|||||||
|
|
||||||
<app-block-overview-multi
|
|
||||||
#blockGraph
|
|
||||||
[isLoading]="false"
|
|
||||||
[numBlocks]="numBlocks"
|
|
||||||
[padding]="padding"
|
|
||||||
[blockWidth]="blockWidth"
|
|
||||||
[resolution]="resolution"
|
|
||||||
[blockLimit]="stateService.blockVSize"
|
|
||||||
[orientation]="'top'"
|
|
||||||
[flip]="false"
|
|
||||||
[animationDuration]="animationDuration"
|
|
||||||
[animationOffset]="animationOffset"
|
|
||||||
[disableSpinner]="true"
|
|
||||||
></app-block-overview-multi>
|
|
||||||
<div class="blocks" [class.wrap]="wrapBlocks">
|
<div class="blocks" [class.wrap]="wrapBlocks">
|
||||||
<ng-container *ngFor="let i of blockIndices">
|
<ng-container *ngFor="let i of blockIndices">
|
||||||
<div class="block-wrapper" [style]="wrapperStyle">
|
<div class="block-wrapper" [style]="wrapperStyle">
|
||||||
<div class="block-container" [style]="containerStyle">
|
<div class="block-container" [style]="containerStyle">
|
||||||
|
<app-block-overview-graph
|
||||||
|
#blockGraph
|
||||||
|
[isLoading]="false"
|
||||||
|
[resolution]="resolution"
|
||||||
|
[blockLimit]="stateService.blockVSize"
|
||||||
|
[orientation]="'top'"
|
||||||
|
[flip]="false"
|
||||||
|
[animationDuration]="animationDuration"
|
||||||
|
[animationOffset]="animationOffset"
|
||||||
|
[disableSpinner]="true"
|
||||||
|
[relativeTime]="blockInfo[i]?.timestamp"
|
||||||
|
(txClickEvent)="onTxClick($event)"
|
||||||
|
></app-block-overview-graph>
|
||||||
<div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
|
<div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
|
||||||
<h1 class="height">{{ blockInfo[i].height }}</h1>
|
<h1 class="height">{{ blockInfo[i].height }}</h1>
|
||||||
<h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }} <img class="pool-logo" [src]="'/resources/mining-pools/' + blockInfo[i].extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'"> </h2>
|
<h2 class="mined-by">by {{ blockInfo[i].extras.pool.name || 'Unknown' }}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
.blocks {
|
.blocks {
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 100vw;
|
min-width: 100vw;
|
||||||
@@ -69,12 +66,4 @@
|
|||||||
.block-container {
|
.block-container {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.pool-logo {
|
|
||||||
width: 1.2em;
|
|
||||||
height: 1.2em;
|
|
||||||
position: relative;
|
|
||||||
top: -1px;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { catchError } from 'rxjs/operators';
|
import { catchError, startWith } from 'rxjs/operators';
|
||||||
import { Subject, Subscription, of } from 'rxjs';
|
import { Subject, Subscription, of } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
|
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||||
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
|
import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
|
||||||
import { detectWebGL } from '../../shared/graphs.utils';
|
import { detectWebGL } from '../../shared/graphs.utils';
|
||||||
import { animate, style, transition, trigger } from '@angular/animations';
|
import { animate, style, transition, trigger } from '@angular/animations';
|
||||||
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
|
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
|
||||||
import { BlockOverviewMultiComponent } from '../block-overview-multi/block-overview-multi.component';
|
|
||||||
import { CacheService } from '../../services/cache.service';
|
|
||||||
|
|
||||||
function bestFitResolution(min, max, n): number {
|
function bestFitResolution(min, max, n): number {
|
||||||
const target = (min + max) / 2;
|
const target = (min + max) / 2;
|
||||||
@@ -48,26 +48,24 @@ interface BlockInfo extends BlockExtended {
|
|||||||
})
|
})
|
||||||
export class EightBlocksComponent implements OnInit, OnDestroy {
|
export class EightBlocksComponent implements OnInit, OnDestroy {
|
||||||
network = '';
|
network = '';
|
||||||
latestBlocks: (BlockExtended | null)[] = [];
|
latestBlocks: BlockExtended[] = [];
|
||||||
pendingBlocks: Record<number, ((b: BlockExtended) => void)[]> = {};
|
|
||||||
isLoadingTransactions = true;
|
isLoadingTransactions = true;
|
||||||
strippedTransactions: { [height: number]: TransactionStripped[] } = {};
|
strippedTransactions: { [height: number]: TransactionStripped[] } = {};
|
||||||
webGlEnabled = true;
|
webGlEnabled = true;
|
||||||
hoverTx: string | null = null;
|
hoverTx: string | null = null;
|
||||||
|
|
||||||
tipSubscription: Subscription;
|
blocksSubscription: Subscription;
|
||||||
cacheBlocksSubscription: Subscription;
|
cacheBlocksSubscription: Subscription;
|
||||||
networkChangedSubscription: Subscription;
|
networkChangedSubscription: Subscription;
|
||||||
queryParamsSubscription: Subscription;
|
queryParamsSubscription: Subscription;
|
||||||
graphChangeSubscription: Subscription;
|
graphChangeSubscription: Subscription;
|
||||||
|
|
||||||
height: number = 0;
|
|
||||||
numBlocks: number = 8;
|
numBlocks: number = 8;
|
||||||
blockIndices: number[] = [...Array(8).keys()];
|
blockIndices: number[] = [...Array(8).keys()];
|
||||||
autofit: boolean = false;
|
autofit: boolean = false;
|
||||||
padding: number = 0;
|
padding: number = 0;
|
||||||
wrapBlocks: boolean = false;
|
wrapBlocks: boolean = false;
|
||||||
blockWidth: number = 360;
|
blockWidth: number = 1080;
|
||||||
animationDuration: number = 2000;
|
animationDuration: number = 2000;
|
||||||
animationOffset: number = 0;
|
animationOffset: number = 0;
|
||||||
stagger: number = 0;
|
stagger: number = 0;
|
||||||
@@ -81,14 +79,13 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
|
|||||||
wrapperStyle = {
|
wrapperStyle = {
|
||||||
'--block-width': '1080px',
|
'--block-width': '1080px',
|
||||||
width: '1080px',
|
width: '1080px',
|
||||||
height: '1080px',
|
|
||||||
maxWidth: '1080px',
|
maxWidth: '1080px',
|
||||||
margin: '',
|
padding: '',
|
||||||
};
|
};
|
||||||
containerStyle = {};
|
containerStyle = {};
|
||||||
resolution: number = 86;
|
resolution: number = 86;
|
||||||
|
|
||||||
@ViewChild('blockGraph') blockGraph: BlockOverviewMultiComponent;
|
@ViewChildren('blockGraph') blockGraphs: QueryList<BlockOverviewGraphComponent>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
@@ -96,7 +93,6 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
|
|||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private cacheService: CacheService,
|
|
||||||
private bytesPipe: BytesPipe,
|
private bytesPipe: BytesPipe,
|
||||||
) {
|
) {
|
||||||
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
|
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
|
||||||
@@ -115,7 +111,7 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
|
|||||||
this.wrapBlocks = params.wrap !== 'false';
|
this.wrapBlocks = params.wrap !== 'false';
|
||||||
this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
|
this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
|
||||||
this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
|
this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
|
||||||
this.animationOffset = 0;
|
this.animationOffset = this.padding * 2;
|
||||||
|
|
||||||
if (this.autofit) {
|
if (this.autofit) {
|
||||||
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
|
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
|
||||||
@@ -126,26 +122,22 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
|
|||||||
this.wrapperStyle = {
|
this.wrapperStyle = {
|
||||||
'--block-width': this.blockWidth + 'px',
|
'--block-width': this.blockWidth + 'px',
|
||||||
width: this.blockWidth + 'px',
|
width: this.blockWidth + 'px',
|
||||||
height: this.blockWidth + 'px',
|
|
||||||
maxWidth: this.blockWidth + 'px',
|
maxWidth: this.blockWidth + 'px',
|
||||||
margin: (this.padding || 0) +'px ',
|
padding: (this.padding || 0) +'px 0px',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.cacheBlocksSubscription = this.cacheService.loadedBlocks$.subscribe((block: BlockExtended) => {
|
|
||||||
if (this.pendingBlocks[block.height]) {
|
|
||||||
this.pendingBlocks[block.height].forEach(resolve => resolve(block));
|
|
||||||
delete this.pendingBlocks[block.height];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.tipSubscription?.unsubscribe();
|
|
||||||
if (params.test === 'true') {
|
if (params.test === 'true') {
|
||||||
|
if (this.blocksSubscription) {
|
||||||
|
this.blocksSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
this.blocksSubscription = (new Subject<BlockExtended[]>()).subscribe((blocks) => {
|
||||||
|
this.handleNewBlock(blocks.slice(0, this.numBlocks));
|
||||||
|
});
|
||||||
this.shiftTestBlocks();
|
this.shiftTestBlocks();
|
||||||
} else {
|
} else if (!this.blocksSubscription) {
|
||||||
this.tipSubscription = this.stateService.chainTip$
|
this.blocksSubscription = this.stateService.blocks$
|
||||||
.subscribe((height) => {
|
.subscribe((blocks) => {
|
||||||
this.height = height;
|
this.handleNewBlock(blocks.slice(0, this.numBlocks));
|
||||||
this.handleNewBlock(height);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -157,13 +149,15 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.setupBlockGraphs();
|
this.graphChangeSubscription = this.blockGraphs.changes.pipe(startWith(null)).subscribe(() => {
|
||||||
|
this.setupBlockGraphs();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.stateService.markBlock$.next({});
|
this.stateService.markBlock$.next({});
|
||||||
if (this.tipSubscription) {
|
if (this.blocksSubscription) {
|
||||||
this.tipSubscription?.unsubscribe();
|
this.blocksSubscription?.unsubscribe();
|
||||||
}
|
}
|
||||||
this.cacheBlocksSubscription?.unsubscribe();
|
this.cacheBlocksSubscription?.unsubscribe();
|
||||||
this.networkChangedSubscription?.unsubscribe();
|
this.networkChangedSubscription?.unsubscribe();
|
||||||
@@ -173,27 +167,32 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
|
|||||||
shiftTestBlocks(): void {
|
shiftTestBlocks(): void {
|
||||||
const sub = this.apiService.getBlocks$(this.testHeight).subscribe(result => {
|
const sub = this.apiService.getBlocks$(this.testHeight).subscribe(result => {
|
||||||
sub.unsubscribe();
|
sub.unsubscribe();
|
||||||
this.handleNewBlock(this.testHeight);
|
this.handleNewBlock(result.slice(0, this.numBlocks));
|
||||||
this.testHeight++;
|
this.testHeight++;
|
||||||
clearTimeout(this.testShiftTimeout);
|
clearTimeout(this.testShiftTimeout);
|
||||||
this.testShiftTimeout = window.setTimeout(() => { this.shiftTestBlocks(); }, 10000);
|
this.testShiftTimeout = window.setTimeout(() => { this.shiftTestBlocks(); }, 10000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleNewBlock(height: number): Promise<void> {
|
async handleNewBlock(blocks: BlockExtended[]): Promise<void> {
|
||||||
const readyPromises: Promise<TransactionStripped[]>[] = [];
|
const readyPromises: Promise<TransactionStripped[]>[] = [];
|
||||||
const previousBlocks = this.latestBlocks;
|
const previousBlocks = this.latestBlocks;
|
||||||
|
|
||||||
const blocks = await this.loadBlocks(height, this.numBlocks);
|
|
||||||
console.log('loaded ', blocks.length, ' blocks from height ', height);
|
|
||||||
console.log(blocks);
|
|
||||||
|
|
||||||
const newHeights = {};
|
const newHeights = {};
|
||||||
this.latestBlocks = blocks;
|
this.latestBlocks = blocks;
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
newHeights[block.height] = true;
|
newHeights[block.height] = true;
|
||||||
if (!this.strippedTransactions[block.height]) {
|
if (!this.strippedTransactions[block.height]) {
|
||||||
readyPromises.push(this.loadBlockTransactions(block));
|
readyPromises.push(new Promise((resolve) => {
|
||||||
|
const subscription = this.apiService.getStrippedBlockTransactions$(block.id).pipe(
|
||||||
|
catchError(() => {
|
||||||
|
return of([]);
|
||||||
|
}),
|
||||||
|
).subscribe((transactions) => {
|
||||||
|
this.strippedTransactions[block.height] = transactions;
|
||||||
|
subscription.unsubscribe();
|
||||||
|
resolve(transactions);
|
||||||
|
});
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Promise.allSettled(readyPromises);
|
await Promise.allSettled(readyPromises);
|
||||||
@@ -207,45 +206,12 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadBlocks(height: number, numBlocks: number): Promise<BlockExtended[]> {
|
|
||||||
console.log('loading ', numBlocks, ' blocks from height ', height);
|
|
||||||
const promises: Promise<BlockExtended>[] = [];
|
|
||||||
for (let i = 0; i < numBlocks; i++) {
|
|
||||||
this.cacheService.loadBlock(height - i);
|
|
||||||
const cachedBlock = this.cacheService.getCachedBlock(height - i);
|
|
||||||
if (cachedBlock) {
|
|
||||||
promises.push(Promise.resolve(cachedBlock));
|
|
||||||
} else {
|
|
||||||
promises.push(new Promise((resolve) => {
|
|
||||||
if (!this.pendingBlocks[height - i]) {
|
|
||||||
this.pendingBlocks[height - i] = [];
|
|
||||||
}
|
|
||||||
this.pendingBlocks[height - i].push(resolve);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Promise.all(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadBlockTransactions(block: BlockExtended): Promise<TransactionStripped[]> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
this.apiService.getStrippedBlockTransactions$(block.id).pipe(
|
|
||||||
catchError(() => {
|
|
||||||
return of([]);
|
|
||||||
}),
|
|
||||||
).subscribe((transactions) => {
|
|
||||||
this.strippedTransactions[block.height] = transactions;
|
|
||||||
resolve(transactions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBlockGraphs(blocks): void {
|
updateBlockGraphs(blocks): void {
|
||||||
const startTime = performance.now() + 1000 - (this.stagger < 0 ? this.stagger * 8 : 0);
|
const startTime = performance.now() + 1000 - (this.stagger < 0 ? this.stagger * 8 : 0);
|
||||||
if (this.blockGraph) {
|
if (this.blockGraphs) {
|
||||||
for (let i = 0; i < this.numBlocks; i++) {
|
this.blockGraphs.forEach((graph, index) => {
|
||||||
this.blockGraph.replace(i, this.strippedTransactions[blocks?.[i]?.height] || [], 'right', false, startTime + (this.stagger * i));
|
graph.replace(this.strippedTransactions[blocks?.[index]?.height] || [], 'right', false, startTime + (this.stagger * index));
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
this.showInfo = false;
|
this.showInfo = false;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -260,11 +226,28 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupBlockGraphs(): void {
|
setupBlockGraphs(): void {
|
||||||
if (this.blockGraph) {
|
if (this.blockGraphs) {
|
||||||
for (let i = 0; i < this.numBlocks; i++) {
|
this.blockGraphs.forEach((graph, index) => {
|
||||||
this.blockGraph.destroy(i);
|
graph.destroy();
|
||||||
this.blockGraph.setup(i, this.strippedTransactions[this.latestBlocks?.[i]?.height] || []);
|
graph.setup(this.strippedTransactions[this.latestBlocks?.[index]?.height] || []);
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTxClick(event: { tx: TransactionStripped, keyModifier: boolean }): void {
|
||||||
|
const url = new RelativeUrlPipe(this.stateService).transform(`/tx/${event.tx.txid}`);
|
||||||
|
if (!event.keyModifier) {
|
||||||
|
this.router.navigate([url]);
|
||||||
|
} else {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTxHover(txid: string): void {
|
||||||
|
if (txid && txid.length) {
|
||||||
|
this.hoverTx = txid;
|
||||||
|
} else {
|
||||||
|
this.hoverTx = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<app-block-overview-multi
|
|
||||||
#blockGraph
|
|
||||||
[isLoading]="false"
|
|
||||||
[numBlocks]="numBlocks"
|
|
||||||
[padding]="padding"
|
|
||||||
[blockWidth]="blockWidth"
|
|
||||||
[resolution]="resolution"
|
|
||||||
[blockLimit]="stateService.blockVSize"
|
|
||||||
[orientation]="'left'"
|
|
||||||
[flip]="true"
|
|
||||||
[animationDuration]="animationDuration"
|
|
||||||
[animationOffset]="animationOffset"
|
|
||||||
[disableSpinner]="true"
|
|
||||||
></app-block-overview-multi>
|
|
||||||
<div class="blocks" [class.wrap]="wrapBlocks">
|
|
||||||
<ng-container *ngFor="let i of blockIndices">
|
|
||||||
<div class="block-wrapper" [style]="wrapperStyle">
|
|
||||||
<div class="block-container" [style]="containerStyle">
|
|
||||||
<div *ngIf="showInfo && blockInfo[i]" class="info" @infoChange>
|
|
||||||
<h1 class="height">{{ blockInfo[i].label }}</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
.blocks {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
align-content: flex-start;
|
|
||||||
|
|
||||||
&.wrap {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-wrapper {
|
|
||||||
flex-grow: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
position: relative;
|
|
||||||
--block-width: 1080px;
|
|
||||||
|
|
||||||
.info {
|
|
||||||
position: absolute;
|
|
||||||
left: 8%;
|
|
||||||
top: 8%;
|
|
||||||
right: 8%;
|
|
||||||
bottom: 8%;
|
|
||||||
height: 84%;
|
|
||||||
width: 84%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: calc(var(--block-width) * 0.03);
|
|
||||||
text-shadow: 0 0 calc(var(--block-width) * 0.05) black;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 6em;
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: calc(var(--block-width) * 0.03);
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 1.8em;
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: calc(var(--block-width) * 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hash {
|
|
||||||
font-family: monospace;
|
|
||||||
word-wrap: break-word;
|
|
||||||
font-size: 1.4em;
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: calc(var(--block-width) * 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mined-by {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
margin: auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.block-container {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { catchError } from 'rxjs/operators';
|
|
||||||
import { Subject, Subscription, of } from 'rxjs';
|
|
||||||
import { StateService } from '../../services/state.service';
|
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
|
||||||
import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
|
|
||||||
import { ApiService } from '../../services/api.service';
|
|
||||||
import { detectWebGL } from '../../shared/graphs.utils';
|
|
||||||
import { animate, style, transition, trigger } from '@angular/animations';
|
|
||||||
import { BytesPipe } from '../../shared/pipes/bytes-pipe/bytes.pipe';
|
|
||||||
import { BlockOverviewMultiComponent } from '../block-overview-multi/block-overview-multi.component';
|
|
||||||
import { CacheService } from '../../services/cache.service';
|
|
||||||
import { isMempoolDelta, MempoolBlockDelta } from '../../interfaces/websocket.interface';
|
|
||||||
|
|
||||||
function bestFitResolution(min, max, n): number {
|
|
||||||
const target = (min + max) / 2;
|
|
||||||
let bestScore = Infinity;
|
|
||||||
let best = null;
|
|
||||||
for (let i = min; i <= max; i++) {
|
|
||||||
const remainder = (n % i);
|
|
||||||
if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
|
|
||||||
bestScore = remainder;
|
|
||||||
best = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-eight-mempool',
|
|
||||||
templateUrl: './eight-mempool.component.html',
|
|
||||||
styleUrls: ['./eight-mempool.component.scss'],
|
|
||||||
animations: [
|
|
||||||
trigger('infoChange', [
|
|
||||||
transition(':enter', [
|
|
||||||
style({ opacity: 0 }),
|
|
||||||
animate('1000ms', style({ opacity: 1 })),
|
|
||||||
]),
|
|
||||||
transition(':leave', [
|
|
||||||
animate('1000ms 500ms', style({ opacity: 0 }))
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class EightMempoolComponent implements OnInit, OnDestroy {
|
|
||||||
network = '';
|
|
||||||
strippedTransactions: { [height: number]: TransactionStripped[] } = {};
|
|
||||||
webGlEnabled = true;
|
|
||||||
hoverTx: string | null = null;
|
|
||||||
|
|
||||||
networkChangedSubscription: Subscription;
|
|
||||||
queryParamsSubscription: Subscription;
|
|
||||||
graphChangeSubscription: Subscription;
|
|
||||||
blockSub: Subscription;
|
|
||||||
mempoolBlockSub: Subscription;
|
|
||||||
|
|
||||||
chainDirection: string = 'right';
|
|
||||||
poolDirection: string = 'left';
|
|
||||||
|
|
||||||
lastBlockHeight: number = 0;
|
|
||||||
lastBlockHeightUpdate: number[] = [];
|
|
||||||
numBlocks: number = 8;
|
|
||||||
blockIndices: number[] = [];
|
|
||||||
autofit: boolean = false;
|
|
||||||
padding: number = 0;
|
|
||||||
wrapBlocks: boolean = false;
|
|
||||||
blockWidth: number = 360;
|
|
||||||
animationDuration: number = 2000;
|
|
||||||
animationOffset: number = 0;
|
|
||||||
stagger: number = 0;
|
|
||||||
testing: boolean = true;
|
|
||||||
testHeight: number = 800000;
|
|
||||||
testShiftTimeout: number;
|
|
||||||
|
|
||||||
showInfo: boolean = true;
|
|
||||||
blockInfo: { label: string}[] = [
|
|
||||||
{ label: '' },
|
|
||||||
{ label: 'mempool' },
|
|
||||||
{ label: 'blocks' },
|
|
||||||
];
|
|
||||||
|
|
||||||
wrapperStyle = {
|
|
||||||
'--block-width': '1080px',
|
|
||||||
width: '1080px',
|
|
||||||
height: '1080px',
|
|
||||||
maxWidth: '1080px',
|
|
||||||
margin: '',
|
|
||||||
};
|
|
||||||
containerStyle = {};
|
|
||||||
resolution: number = 86;
|
|
||||||
|
|
||||||
@ViewChild('blockGraph') blockGraph: BlockOverviewMultiComponent;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router,
|
|
||||||
public stateService: StateService,
|
|
||||||
private websocketService: WebsocketService,
|
|
||||||
private apiService: ApiService,
|
|
||||||
private cacheService: CacheService,
|
|
||||||
private bytesPipe: BytesPipe,
|
|
||||||
) {
|
|
||||||
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
|
||||||
this.network = this.stateService.network;
|
|
||||||
|
|
||||||
this.blockSub = this.stateService.mempoolBlockUpdate$.subscribe((update) => {
|
|
||||||
// process update
|
|
||||||
if (isMempoolDelta(update)) {
|
|
||||||
// delta
|
|
||||||
this.updateBlock(update);
|
|
||||||
} else {
|
|
||||||
const transactionsStripped = update.transactions;
|
|
||||||
const inOldBlock = {};
|
|
||||||
const inNewBlock = {};
|
|
||||||
const added: TransactionStripped[] = [];
|
|
||||||
const changed: { txid: string, rate: number | undefined, flags: number, acc: boolean | undefined }[] = [];
|
|
||||||
const removed: string[] = [];
|
|
||||||
for (const tx of transactionsStripped) {
|
|
||||||
inNewBlock[tx.txid] = true;
|
|
||||||
}
|
|
||||||
for (const txid of Object.keys(this.blockGraph?.scenes[this.numBlocks - update.block - 1]?.txs || {})) {
|
|
||||||
inOldBlock[txid] = true;
|
|
||||||
if (!inNewBlock[txid]) {
|
|
||||||
removed.push(txid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const tx of transactionsStripped) {
|
|
||||||
if (!inOldBlock[tx.txid]) {
|
|
||||||
added.push(tx);
|
|
||||||
} else {
|
|
||||||
changed.push({
|
|
||||||
txid: tx.txid,
|
|
||||||
rate: tx.rate,
|
|
||||||
flags: tx.flags,
|
|
||||||
acc: tx.acc
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.updateBlock({
|
|
||||||
block: update.block,
|
|
||||||
removed,
|
|
||||||
changed,
|
|
||||||
added
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.mempoolBlockSub = this.stateService.mempoolBlocks$.subscribe((blocks) => {
|
|
||||||
this.blockInfo[0].label = `+${blocks.length - this.numBlocks}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
|
|
||||||
this.numBlocks = Number.isInteger(Number(params.numBlocks)) ? Number(params.numBlocks) : 8;
|
|
||||||
this.blockIndices = [...Array(this.numBlocks).keys()];
|
|
||||||
this.lastBlockHeightUpdate = this.blockIndices.map(() => 0);
|
|
||||||
this.autofit = params.autofit !== 'false';
|
|
||||||
this.blockWidth = Number.isInteger(Number(params.blockWidth)) ? Number(params.blockWidth) : 540;
|
|
||||||
this.padding = Number.isInteger(Number(params.padding)) ? Number(params.padding) : 0;
|
|
||||||
this.wrapBlocks = params.wrap !== 'false';
|
|
||||||
this.stagger = Number.isInteger(Number(params.stagger)) ? Number(params.stagger) : 0;
|
|
||||||
this.animationDuration = Number.isInteger(Number(params.animationDuration)) ? Number(params.animationDuration) : 2000;
|
|
||||||
this.animationOffset = 0;
|
|
||||||
|
|
||||||
if (this.autofit) {
|
|
||||||
this.resolution = bestFitResolution(76, 96, this.blockWidth - this.padding * 2);
|
|
||||||
} else {
|
|
||||||
this.resolution = 86;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.wrapperStyle = {
|
|
||||||
'--block-width': this.blockWidth + 'px',
|
|
||||||
width: this.blockWidth + 'px',
|
|
||||||
height: this.blockWidth + 'px',
|
|
||||||
maxWidth: this.blockWidth + 'px',
|
|
||||||
margin: (this.padding || 0) +'px ',
|
|
||||||
};
|
|
||||||
|
|
||||||
this.websocketService.startTrackMempoolBlocks(this.blockIndices);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.networkChangedSubscription = this.stateService.networkChanged$
|
|
||||||
.subscribe((network) => this.network = network);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.stateService.markBlock$.next({});
|
|
||||||
this.blockSub.unsubscribe();
|
|
||||||
this.mempoolBlockSub.unsubscribe();
|
|
||||||
this.networkChangedSubscription?.unsubscribe();
|
|
||||||
this.queryParamsSubscription?.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBlock(delta: MempoolBlockDelta): void {
|
|
||||||
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeightUpdate[delta.block]);
|
|
||||||
if (blockMined) {
|
|
||||||
this.blockGraph.update(this.numBlocks - delta.block - 1, delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined);
|
|
||||||
} else {
|
|
||||||
this.blockGraph.update(this.numBlocks - delta.block - 1, delta.added, delta.removed, delta.changed || [], this.poolDirection);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastBlockHeightUpdate[delta.block] = this.stateService.latestBlockHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -608,7 +608,7 @@
|
|||||||
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
<td class="td-width" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||||
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
<td class="text-wrap">{{ tx.fee | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||||
@if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
|
@if (accelerationInfo?.bidBoost ?? tx.feeDelta > 0) {
|
||||||
<span class="oobFees" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sats">sats</span>
|
<span class="oobFees" [attr.data-cy]="'tx-fee-delta'" i18n-ngbTooltip="Acceleration Fees" ngbTooltip="Acceleration fees paid out-of-band"> +{{ accelerationInfo?.bidBoost ?? tx.feeDelta | number }} </span><span class="symbol" i18n="shared.sats">sats</span>
|
||||||
}
|
}
|
||||||
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0)"></app-fiat></span>
|
<span class="fiat"><app-fiat [blockConversion]="tx.price" [value]="tx.fee + ((accelerationInfo?.bidBoost ?? tx.feeDelta) || 0)"></app-fiat></span>
|
||||||
</td>
|
</td>
|
||||||
@@ -647,9 +647,9 @@
|
|||||||
<td>
|
<td>
|
||||||
<div class="effective-fee-container">
|
<div class="effective-fee-container">
|
||||||
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize || tx.acceleration)) {
|
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize || tx.acceleration)) {
|
||||||
<app-fee-rate [class.oobFees]="isAcceleration" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
<app-fee-rate [attr.data-cy]="'fee-rate'" [class.oobFees]="isAcceleration" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||||
} @else {
|
} @else {
|
||||||
<app-fee-rate [class.oobFees]="isAcceleration" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
<app-fee-rate [attr.data-cy]="'fee-rate'" [class.oobFees]="isAcceleration" [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (tx?.status?.confirmed && !tx.acceleration && !accelerationInfo && tx.fee && tx.effectiveFeePerVsize) {
|
@if (tx?.status?.confirmed && !tx.acceleration && !accelerationInfo && tx.fee && tx.effectiveFeePerVsize) {
|
||||||
@@ -670,7 +670,7 @@
|
|||||||
<ng-template #acceleratingRow>
|
<ng-template #acceleratingRow>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="2" colspan="2" style="padding: 0;">
|
<td rowspan="2" colspan="2" style="padding: 0;">
|
||||||
<app-active-acceleration-box [acceleratedBy]="tx.acceleratedBy" [effectiveFeeRate]="tx.effectiveFeePerVsize" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [hasCpfp]="hasCpfp" (toggleCpfp)="showCpfpDetails = !showCpfpDetails" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
|
<app-active-acceleration-box [attr.data-cy]="'active-acceleration-box'" [acceleratedBy]="tx.acceleratedBy" [effectiveFeeRate]="tx.effectiveFeePerVsize" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats" [hasCpfp]="hasCpfp" (toggleCpfp)="showCpfpDetails = !showCpfpDetails" [chartPositionLeft]="isMobile"></app-active-acceleration-box>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr></tr>
|
<tr></tr>
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export interface WebsocketResponse {
|
|||||||
'track-scriptpubkeys'?: string[];
|
'track-scriptpubkeys'?: string[];
|
||||||
'track-asset'?: string;
|
'track-asset'?: string;
|
||||||
'track-mempool-block'?: number;
|
'track-mempool-block'?: number;
|
||||||
'track-mempool-blocks'?: number[];
|
|
||||||
'track-rbf'?: string;
|
'track-rbf'?: string;
|
||||||
'track-rbf-summary'?: boolean;
|
'track-rbf-summary'?: boolean;
|
||||||
'track-accelerations'?: boolean;
|
'track-accelerations'?: boolean;
|
||||||
|
|||||||
@@ -29,14 +29,12 @@ export class WebsocketService {
|
|||||||
private isTrackingTx = false;
|
private isTrackingTx = false;
|
||||||
private trackingTxId: string;
|
private trackingTxId: string;
|
||||||
private isTrackingMempoolBlock = false;
|
private isTrackingMempoolBlock = false;
|
||||||
private isTrackingMempoolBlocks = false;
|
|
||||||
private isTrackingRbf: 'all' | 'fullRbf' | false = false;
|
private isTrackingRbf: 'all' | 'fullRbf' | false = false;
|
||||||
private isTrackingRbfSummary = false;
|
private isTrackingRbfSummary = false;
|
||||||
private isTrackingAddress: string | false = false;
|
private isTrackingAddress: string | false = false;
|
||||||
private isTrackingAddresses: string[] | false = false;
|
private isTrackingAddresses: string[] | false = false;
|
||||||
private isTrackingAccelerations: boolean = false;
|
private isTrackingAccelerations: boolean = false;
|
||||||
private trackingMempoolBlock: number;
|
private trackingMempoolBlock: number;
|
||||||
private trackingMempoolBlocks: number[];
|
|
||||||
private stoppingTrackMempoolBlock: any | null = null;
|
private stoppingTrackMempoolBlock: any | null = null;
|
||||||
private latestGitCommit = '';
|
private latestGitCommit = '';
|
||||||
private onlineCheckTimeout: number;
|
private onlineCheckTimeout: number;
|
||||||
@@ -124,9 +122,6 @@ export class WebsocketService {
|
|||||||
if (this.isTrackingMempoolBlock) {
|
if (this.isTrackingMempoolBlock) {
|
||||||
this.startTrackMempoolBlock(this.trackingMempoolBlock, true);
|
this.startTrackMempoolBlock(this.trackingMempoolBlock, true);
|
||||||
}
|
}
|
||||||
if (this.isTrackingMempoolBlocks) {
|
|
||||||
this.startTrackMempoolBlocks(this.trackingMempoolBlocks);
|
|
||||||
}
|
|
||||||
if (this.isTrackingRbf) {
|
if (this.isTrackingRbf) {
|
||||||
this.startTrackRbf(this.isTrackingRbf);
|
this.startTrackRbf(this.isTrackingRbf);
|
||||||
}
|
}
|
||||||
@@ -223,13 +218,6 @@ export class WebsocketService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
startTrackMempoolBlocks(blocks: number[], force: boolean = false): boolean {
|
|
||||||
this.websocketSubject.next({ 'track-mempool-blocks': blocks });
|
|
||||||
this.isTrackingMempoolBlocks = true;
|
|
||||||
this.trackingMempoolBlocks = blocks;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
stopTrackMempoolBlock(): void {
|
stopTrackMempoolBlock(): void {
|
||||||
if (this.stoppingTrackMempoolBlock) {
|
if (this.stoppingTrackMempoolBlock) {
|
||||||
clearTimeout(this.stoppingTrackMempoolBlock);
|
clearTimeout(this.stoppingTrackMempoolBlock);
|
||||||
@@ -243,11 +231,6 @@ export class WebsocketService {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
stopTrackMempoolBlocks(): void {
|
|
||||||
this.websocketSubject.next({ 'track-mempool-blocks': [] });
|
|
||||||
this.isTrackingMempoolBlocks = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
startTrackRbf(mode: 'all' | 'fullRbf') {
|
startTrackRbf(mode: 'all' | 'fullRbf') {
|
||||||
this.websocketSubject.next({ 'track-rbf': mode });
|
this.websocketSubject.next({ 'track-rbf': mode });
|
||||||
this.isTrackingRbf = mode;
|
this.isTrackingRbf = mode;
|
||||||
@@ -450,25 +433,20 @@ export class WebsocketService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (response['projected-block-transactions']) {
|
if (response['projected-block-transactions']) {
|
||||||
if (response['projected-block-transactions'].index != null) {
|
if (response['projected-block-transactions'].index == this.trackingMempoolBlock) {
|
||||||
const update = response['projected-block-transactions'];
|
if (response['projected-block-transactions'].blockTransactions) {
|
||||||
if (update.blockTransactions) {
|
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
|
||||||
this.stateService.mempoolBlockUpdate$.next({
|
this.stateService.mempoolBlockUpdate$.next({
|
||||||
block: update.index,
|
block: this.trackingMempoolBlock,
|
||||||
transactions: update.blockTransactions.map(uncompressTx),
|
transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx),
|
||||||
});
|
});
|
||||||
} else if (update.delta) {
|
} else if (response['projected-block-transactions'].delta) {
|
||||||
this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(update.index, update.delta));
|
if (this.stateService.mempoolSequence && response['projected-block-transactions'].sequence !== this.stateService.mempoolSequence + 1) {
|
||||||
}
|
this.stateService.mempoolSequence = 0;
|
||||||
} else if (response['projected-block-transactions'].length) {
|
this.startTrackMempoolBlock(this.trackingMempoolBlock, true);
|
||||||
for (const update of response['projected-block-transactions']) {
|
} else {
|
||||||
if (update.blockTransactions) {
|
this.stateService.mempoolSequence = response['projected-block-transactions'].sequence;
|
||||||
this.stateService.mempoolBlockUpdate$.next({
|
this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(this.trackingMempoolBlock, response['projected-block-transactions'].delta));
|
||||||
block: update.index,
|
|
||||||
transactions: update.blockTransactions.map(uncompressTx),
|
|
||||||
});
|
|
||||||
} else if (update.delta) {
|
|
||||||
this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(update.index, update.delta));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ import { CapAddressPipe } from './pipes/cap-address-pipe/cap-address-pipe';
|
|||||||
import { StartComponent } from '../components/start/start.component';
|
import { StartComponent } from '../components/start/start.component';
|
||||||
import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component';
|
import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component';
|
||||||
import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component';
|
import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component';
|
||||||
import { BlockOverviewMultiComponent } from '../components/block-overview-multi/block-overview-multi.component';
|
|
||||||
import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component';
|
import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component';
|
||||||
import { BlockFiltersComponent } from '../components/block-filters/block-filters.component';
|
import { BlockFiltersComponent } from '../components/block-filters/block-filters.component';
|
||||||
import { AddressGroupComponent } from '../components/address-group/address-group.component';
|
import { AddressGroupComponent } from '../components/address-group/address-group.component';
|
||||||
@@ -106,7 +105,6 @@ import { AccelerationSparklesComponent } from '../components/acceleration/sparkl
|
|||||||
|
|
||||||
import { BlockViewComponent } from '../components/block-view/block-view.component';
|
import { BlockViewComponent } from '../components/block-view/block-view.component';
|
||||||
import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component';
|
import { EightBlocksComponent } from '../components/eight-blocks/eight-blocks.component';
|
||||||
import { EightMempoolComponent } from '../components/eight-mempool/eight-mempool.component';
|
|
||||||
import { MempoolBlockViewComponent } from '../components/mempool-block-view/mempool-block-view.component';
|
import { MempoolBlockViewComponent } from '../components/mempool-block-view/mempool-block-view.component';
|
||||||
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
|
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
|
||||||
import { ClockchainComponent } from '../components/clockchain/clockchain.component';
|
import { ClockchainComponent } from '../components/clockchain/clockchain.component';
|
||||||
@@ -157,7 +155,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||||||
BlockchainComponent,
|
BlockchainComponent,
|
||||||
BlockViewComponent,
|
BlockViewComponent,
|
||||||
EightBlocksComponent,
|
EightBlocksComponent,
|
||||||
EightMempoolComponent,
|
|
||||||
MempoolBlockViewComponent,
|
MempoolBlockViewComponent,
|
||||||
MempoolBlocksComponent,
|
MempoolBlocksComponent,
|
||||||
BlockchainBlocksComponent,
|
BlockchainBlocksComponent,
|
||||||
@@ -166,7 +163,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||||||
PreviewTitleComponent,
|
PreviewTitleComponent,
|
||||||
StartComponent,
|
StartComponent,
|
||||||
BlockOverviewGraphComponent,
|
BlockOverviewGraphComponent,
|
||||||
BlockOverviewMultiComponent,
|
|
||||||
BlockOverviewTooltipComponent,
|
BlockOverviewTooltipComponent,
|
||||||
BlockFiltersComponent,
|
BlockFiltersComponent,
|
||||||
TransactionsListComponent,
|
TransactionsListComponent,
|
||||||
@@ -221,7 +217,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||||||
BitcoinsatoshisPipe,
|
BitcoinsatoshisPipe,
|
||||||
BlockViewComponent,
|
BlockViewComponent,
|
||||||
EightBlocksComponent,
|
EightBlocksComponent,
|
||||||
EightMempoolComponent,
|
|
||||||
MempoolBlockViewComponent,
|
MempoolBlockViewComponent,
|
||||||
MempoolBlockOverviewComponent,
|
MempoolBlockOverviewComponent,
|
||||||
ClockchainComponent,
|
ClockchainComponent,
|
||||||
@@ -311,7 +306,6 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||||||
AmountComponent,
|
AmountComponent,
|
||||||
StartComponent,
|
StartComponent,
|
||||||
BlockOverviewGraphComponent,
|
BlockOverviewGraphComponent,
|
||||||
BlockOverviewMultiComponent,
|
|
||||||
BlockOverviewTooltipComponent,
|
BlockOverviewTooltipComponent,
|
||||||
BlockFiltersComponent,
|
BlockFiltersComponent,
|
||||||
TransactionsListComponent,
|
TransactionsListComponent,
|
||||||
|
|||||||
Reference in New Issue
Block a user