Compare commits
50 Commits
natsoni/va
...
mononaut/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a4ab30c95 | ||
|
|
9b9aaed757 | ||
|
|
b699063153 | ||
|
|
6947e19ca9 | ||
|
|
a0d3afb4d2 | ||
|
|
67afda7dcf | ||
|
|
a56af00500 | ||
|
|
e3971af207 | ||
|
|
f17635193a | ||
|
|
1c73dc59f9 | ||
|
|
3adbba2959 | ||
|
|
ea1629fba8 | ||
|
|
87a4c087e5 | ||
|
|
692edea1ce | ||
|
|
11cfb8a783 | ||
|
|
0b953f21b0 | ||
|
|
d5508872dd | ||
|
|
321181d708 | ||
|
|
f3bd50d4ab | ||
|
|
12a843c386 | ||
|
|
21f91bcb6e | ||
|
|
d57bd56743 | ||
|
|
08969592ea | ||
|
|
f0437886ee | ||
|
|
cfedb5fd24 | ||
|
|
a9ad892495 | ||
|
|
00838ea947 | ||
|
|
7761ea53c6 | ||
|
|
aeeb4af9ba | ||
|
|
9186f664da | ||
|
|
83db2a3b72 | ||
|
|
3cfd54b4c5 | ||
|
|
c6db016c99 | ||
|
|
6f6a9ea1a4 | ||
|
|
83246be962 | ||
|
|
dcd94d868a | ||
|
|
e9fc5c0433 | ||
|
|
e281684ca4 | ||
|
|
6a915c0b88 | ||
|
|
078dc8d9a1 | ||
|
|
232f81b906 | ||
|
|
8701119304 | ||
|
|
33c9f4a8dc | ||
|
|
0654872627 | ||
|
|
cca798eeaa | ||
|
|
1498db3b33 | ||
|
|
05b022dec8 | ||
|
|
6c6c18830c | ||
|
|
69786d5b4b | ||
|
|
8b1acbe13b |
14
backend/package-lock.json
generated
14
backend/package-lock.json
generated
@@ -18,7 +18,7 @@
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.19.2",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.9.7",
|
||||
"mysql2": "~3.10.0",
|
||||
"redis": "^4.6.6",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
@@ -6197,9 +6197,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.9.7",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
|
||||
"integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.0.tgz",
|
||||
"integrity": "sha512-qx0mfWYt1DpTPkw8mAcHW/OwqqyNqBLBHvY5IjN8+icIYTjt6znrgYJ+gxqNNRpVknb5Wc/gcCM4XjbCR0j5tw==",
|
||||
"dependencies": {
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
@@ -12382,9 +12382,9 @@
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"mysql2": {
|
||||
"version": "3.9.7",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
|
||||
"integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.0.tgz",
|
||||
"integrity": "sha512-qx0mfWYt1DpTPkw8mAcHW/OwqqyNqBLBHvY5IjN8+icIYTjt6znrgYJ+gxqNNRpVknb5Wc/gcCM4XjbCR0j5tw==",
|
||||
"requires": {
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.19.2",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.9.7",
|
||||
"mysql2": "~3.10.0",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"redis": "^4.6.6",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
|
||||
@@ -160,7 +160,8 @@ class BitcoinRoutes {
|
||||
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
|
||||
sigops: tx.sigops,
|
||||
adjustedVsize: tx.adjustedVsize,
|
||||
acceleration: tx.acceleration
|
||||
acceleration: tx.acceleration,
|
||||
acceleratedBy: tx.acceleratedBy || undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import config from '../config';
|
||||
import { Worker } from 'worker_threads';
|
||||
import path from 'path';
|
||||
import mempool from './mempool';
|
||||
import { Acceleration } from './services/acceleration';
|
||||
|
||||
const MAX_UINT32 = Math.pow(2, 32) - 1;
|
||||
|
||||
@@ -333,7 +334,7 @@ class MempoolBlocks {
|
||||
}
|
||||
}
|
||||
|
||||
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], candidates: GbtCandidates | undefined, accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
|
||||
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], candidates: GbtCandidates | undefined, accelerations: { [txid: string]: Acceleration }, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
|
||||
for (const txid of Object.keys(candidates?.txs ?? mempool)) {
|
||||
if (txid in mempool) {
|
||||
mempool[txid].cpfpDirty = false;
|
||||
@@ -396,7 +397,7 @@ class MempoolBlocks {
|
||||
}
|
||||
}
|
||||
|
||||
const isAccelerated : { [txid: string]: boolean } = {};
|
||||
const isAcceleratedBy : { [txid: string]: number[] | false } = {};
|
||||
|
||||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||
// update this thread's mempool with the results
|
||||
@@ -427,17 +428,19 @@ class MempoolBlocks {
|
||||
};
|
||||
|
||||
const acceleration = accelerations[txid];
|
||||
if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
||||
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
||||
if (!mempoolTx.acceleration) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
}
|
||||
mempoolTx.acceleration = true;
|
||||
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
|
||||
for (const ancestor of mempoolTx.ancestors || []) {
|
||||
if (!mempool[ancestor.txid].acceleration) {
|
||||
mempool[ancestor.txid].cpfpDirty = true;
|
||||
}
|
||||
mempool[ancestor.txid].acceleration = true;
|
||||
isAccelerated[ancestor.txid] = true;
|
||||
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
|
||||
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
|
||||
}
|
||||
} else {
|
||||
if (mempoolTx.acceleration) {
|
||||
|
||||
@@ -227,10 +227,8 @@ class MiningRoutes {
|
||||
throw new Error('from must be less than to');
|
||||
}
|
||||
const blockFees = await mining.$getBlockFeesTimespan(parseInt(req.query.from as string, 10), parseInt(req.query.to as string, 10));
|
||||
const blockCount = await BlocksRepository.$blockCount(null, null);
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.header('X-total-count', blockCount.toString());
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(blockFees);
|
||||
} catch (e) {
|
||||
|
||||
@@ -820,6 +820,7 @@ class WebsocketHandler {
|
||||
position: {
|
||||
...mempoolTx.position,
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
}
|
||||
};
|
||||
if (!mempoolTx.cpfpChecked && !mempoolTx.acceleration) {
|
||||
@@ -858,6 +859,7 @@ class WebsocketHandler {
|
||||
txInfo.position = {
|
||||
...mempoolTx.position,
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
};
|
||||
if (!mempoolTx.cpfpChecked) {
|
||||
calculateCpfp(mempoolTx, newMempool);
|
||||
@@ -1134,6 +1136,7 @@ class WebsocketHandler {
|
||||
position: {
|
||||
...mempoolTx.position,
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1153,6 +1156,7 @@ class WebsocketHandler {
|
||||
...mempoolTx.position,
|
||||
},
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
||||
vsize: number,
|
||||
};
|
||||
acceleration?: boolean;
|
||||
acceleratedBy?: number[];
|
||||
replacement?: boolean;
|
||||
uid?: number;
|
||||
flags?: number;
|
||||
@@ -432,7 +433,7 @@ export interface OptimizedStatistic {
|
||||
|
||||
export interface TxTrackingInfo {
|
||||
replacedBy?: string,
|
||||
position?: { block: number, vsize: number, accelerated?: boolean },
|
||||
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[] },
|
||||
cpfp?: {
|
||||
ancestors?: Ancestor[],
|
||||
bestDescendant?: Ancestor | null,
|
||||
@@ -443,6 +444,7 @@ export interface TxTrackingInfo {
|
||||
},
|
||||
utxoSpent?: { [vout: number]: { vin: number, txid: string } },
|
||||
accelerated?: boolean,
|
||||
acceleratedBy?: number[],
|
||||
confirmed?: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
<ng-container>
|
||||
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
|
||||
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
|
||||
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
</a>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@@ -201,7 +201,7 @@
|
||||
<div class="wrapper">
|
||||
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
|
||||
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
|
||||
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '?md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
<img class="image" [src]="'/api/v1/services/account/images/' + sponsor.username + '/md5=' + sponsor.imageMd5" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||
</a>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="td-width field-label" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td>
|
||||
<td class="field-value">
|
||||
<div class="effective-fee-container">
|
||||
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
|
||||
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||
} @else {
|
||||
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td class="pie-chart" rowspan="2">
|
||||
<div class="chart-container">
|
||||
@if (tx && (tx.acceleratedBy || accelerationInfo) && miningStats) {
|
||||
<div
|
||||
echarts
|
||||
*browserOnly
|
||||
class="chart"
|
||||
[initOpts]="chartInitOptions"
|
||||
[options]="chartOptions"
|
||||
style="height: 72px; width: 72px;"
|
||||
(chartInit)="onChartInit($event)"
|
||||
></div>
|
||||
} @else {
|
||||
<div class="chart-loading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="td-width field-label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
|
||||
<td class="field-value" *ngIf="acceleratedByPercentage">
|
||||
{{ acceleratedByPercentage }} <span class="symbol hashrate-label">of hashrate</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,50 @@
|
||||
.td-width {
|
||||
width: 150px;
|
||||
min-width: 150px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
width: 175px;
|
||||
min-width: 175px;
|
||||
}
|
||||
}
|
||||
|
||||
.field-label {
|
||||
@media (max-width: 849px) {
|
||||
text-align: left;
|
||||
}
|
||||
@media (max-width: 649px) {
|
||||
width: auto;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.field-value {
|
||||
@media (max-width: 849px) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hashrate-label {
|
||||
@media (max-width: 420px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pie-chart {
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
|
||||
.chart-container {
|
||||
width: 72px;
|
||||
height: 100%;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 850px) {
|
||||
width: 150px;
|
||||
}
|
||||
@media (max-width: 420px) {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Transaction } from '../../../interfaces/electrs.interface';
|
||||
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
||||
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
|
||||
import { MiningStats } from '../../../services/mining.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-active-acceleration-box',
|
||||
templateUrl: './active-acceleration-box.component.html',
|
||||
styleUrls: ['./active-acceleration-box.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ActiveAccelerationBox implements OnChanges {
|
||||
@Input() tx: Transaction;
|
||||
@Input() accelerationInfo: Acceleration;
|
||||
@Input() miningStats: MiningStats;
|
||||
|
||||
acceleratedByPercentage: string = '';
|
||||
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
};
|
||||
timespan = '';
|
||||
chartInstance: any = undefined;
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (this.tx && (this.tx.acceleratedBy || this.accelerationInfo) && this.miningStats) {
|
||||
this.prepareChartOptions();
|
||||
}
|
||||
}
|
||||
|
||||
getChartData() {
|
||||
const data: object[] = [];
|
||||
const pools: { [id: number]: SinglePoolStats } = {};
|
||||
for (const pool of this.miningStats.pools) {
|
||||
pools[pool.poolUniqueId] = pool;
|
||||
}
|
||||
|
||||
const getDataItem = (value, color, tooltip) => ({
|
||||
value,
|
||||
itemStyle: {
|
||||
color,
|
||||
borderColor: 'rgba(0,0,0,0)',
|
||||
borderWidth: 1,
|
||||
},
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
labelLine: {
|
||||
show: false
|
||||
},
|
||||
emphasis: {
|
||||
disabled: true,
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
borderRadius: 4,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
textStyle: {
|
||||
color: 'var(--tooltip-grey)',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: () => {
|
||||
return tooltip;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let totalAcceleratedHashrate = 0;
|
||||
for (const poolId of (this.accelerationInfo?.pools || this.tx.acceleratedBy || [])) {
|
||||
const pool = pools[poolId];
|
||||
if (!pool) {
|
||||
continue;
|
||||
}
|
||||
totalAcceleratedHashrate += parseFloat(pool.lastEstimatedHashrate);
|
||||
}
|
||||
this.acceleratedByPercentage = ((totalAcceleratedHashrate / parseFloat(this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
|
||||
data.push(getDataItem(
|
||||
totalAcceleratedHashrate,
|
||||
'var(--mainnet-alt)',
|
||||
`${this.acceleratedByPercentage} accelerating`,
|
||||
) as PieSeriesOption);
|
||||
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / parseFloat(this.miningStats.lastEstimatedHashrate))) * 100).toFixed(1) + '%';
|
||||
data.push(getDataItem(
|
||||
(parseFloat(this.miningStats.lastEstimatedHashrate) - totalAcceleratedHashrate),
|
||||
'rgba(127, 127, 127, 0.3)',
|
||||
`${notAcceleratedByPercentage} not accelerating`,
|
||||
) as PieSeriesOption);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
prepareChartOptions() {
|
||||
this.chartOptions = {
|
||||
animation: false,
|
||||
grid: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'item',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: '100%',
|
||||
data: this.getChartData(),
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
if (this.chartInstance !== undefined) {
|
||||
return;
|
||||
}
|
||||
this.chartInstance = ec;
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export class BlockFeesSubsidyGraphComponent implements OnInit {
|
||||
formatNumber = formatNumber;
|
||||
timespan = '';
|
||||
chartInstance: any = undefined;
|
||||
showFiat = false;
|
||||
displayMode: 'normal' | 'fiat' | 'percentage' = 'normal';
|
||||
updateZoom = false;
|
||||
zoomSpan = 100;
|
||||
zoomTimeSpan = '';
|
||||
@@ -106,8 +106,10 @@ export class BlockFeesSubsidyGraphComponent implements OnInit {
|
||||
blockHeight: response.body.map(val => val.avgHeight),
|
||||
blockFees: response.body.map(val => val.avgFees / 100_000_000),
|
||||
blockFeesFiat: response.body.filter(val => val['USD'] > 0).map(val => val.avgFees / 100_000_000 * val['USD']),
|
||||
blockSubsidy: response.body.map(val => this.subsidies[Math.floor(Math.min(val.avgHeight / 210000, 33))] / 100_000_000),
|
||||
blockSubsidyFiat: response.body.filter(val => val['USD'] > 0).map(val => this.subsidies[Math.floor(Math.min(val.avgHeight / 210000, 33))] / 100_000_000 * val['USD']),
|
||||
blockFeesPercent: response.body.map(val => val.avgFees / (val.avgFees + this.subsidyAt(val.avgHeight)) * 100),
|
||||
blockSubsidy: response.body.map(val => this.subsidyAt(val.avgHeight) / 100_000_000),
|
||||
blockSubsidyFiat: response.body.filter(val => val['USD'] > 0).map(val => this.subsidyAt(val.avgHeight) / 100_000_000 * val['USD']),
|
||||
blockSubsidyPercent: response.body.map(val => this.subsidyAt(val.avgHeight) / (val.avgFees + this.subsidyAt(val.avgHeight)) * 100),
|
||||
};
|
||||
|
||||
this.prepareChartOptions();
|
||||
@@ -157,9 +159,9 @@ export class BlockFeesSubsidyGraphComponent implements OnInit {
|
||||
axisPointer: {
|
||||
type: 'line'
|
||||
},
|
||||
backgroundColor: 'color-mix(in srgb, var(--active-bg) 95%, transparent)',
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
borderRadius: 4,
|
||||
shadowColor: 'color-mix(in srgb, var(--active-bg) 95%, transparent)',
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
textStyle: {
|
||||
color: 'var(--tooltip-grey)',
|
||||
align: 'left',
|
||||
@@ -172,11 +174,13 @@ export class BlockFeesSubsidyGraphComponent implements OnInit {
|
||||
let tooltip = `<b style="color: white; margin-left: 2px">${formatterXAxis(this.locale, this.zoomTimeSpan, parseInt(this.data.timestamp[data[0].dataIndex], 10))}</b><br>`;
|
||||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
const tick = data[i];
|
||||
if (!this.showFiat) tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data, this.locale, '1.0-3')} BTC<br>`;
|
||||
else tooltip += `${tick.marker} ${tick.seriesName}: ${this.fiatCurrencyPipe.transform(tick.data, null, 'USD') }<br>`;
|
||||
tooltip += `${tick.marker} ${tick.seriesName.split(' ')[0]}: `;
|
||||
if (this.displayMode === 'normal') tooltip += `${formatNumber(tick.data, this.locale, '1.0-3')} BTC<br>`;
|
||||
else if (this.displayMode === 'fiat') tooltip += `${this.fiatCurrencyPipe.transform(tick.data, null, 'USD') }<br>`;
|
||||
else tooltip += `${formatNumber(tick.data, this.locale, '1.0-2')}%<br>`;
|
||||
}
|
||||
if (!this.showFiat) tooltip += `<div style="margin-left: 2px">${formatNumber(data.reduce((acc, val) => acc + val.data, 0), this.locale, '1.0-3')} BTC</div>`;
|
||||
else tooltip += `<div style="margin-left: 2px">${this.fiatCurrencyPipe.transform(data.reduce((acc, val) => acc + val.data, 0), null, 'USD')}</div>`;
|
||||
if (this.displayMode === 'normal') tooltip += `<div style="margin-left: 2px">${formatNumber(data.reduce((acc, val) => acc + val.data, 0), this.locale, '1.0-3')} BTC</div>`;
|
||||
else if (this.displayMode === 'fiat') tooltip += `<div style="margin-left: 2px">${this.fiatCurrencyPipe.transform(data.reduce((acc, val) => acc + val.data, 0), null, 'USD')}</div>`;
|
||||
if (['24h', '3d'].includes(this.zoomTimeSpan)) {
|
||||
tooltip += `<small>` + $localize`At block <b style="color: white; margin-left: 2px">${data[0].axisValue}` + `</small>`;
|
||||
} else {
|
||||
@@ -250,12 +254,30 @@ export class BlockFeesSubsidyGraphComponent implements OnInit {
|
||||
},
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: 'Subsidy (%)',
|
||||
inactiveColor: 'var(--grey)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: 'Fees (%)',
|
||||
inactiveColor: 'var(--grey)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
},
|
||||
],
|
||||
selected: {
|
||||
'Subsidy (USD)': this.showFiat,
|
||||
'Fees (USD)': this.showFiat,
|
||||
'Subsidy': !this.showFiat,
|
||||
'Fees': !this.showFiat,
|
||||
'Subsidy (USD)': this.displayMode === 'fiat',
|
||||
'Fees (USD)': this.displayMode === 'fiat',
|
||||
'Subsidy': this.displayMode === 'normal',
|
||||
'Fees': this.displayMode === 'normal',
|
||||
'Subsidy (%)': this.displayMode === 'percentage',
|
||||
'Fees (%)': this.displayMode === 'percentage',
|
||||
},
|
||||
},
|
||||
yAxis: this.data.blockFees.length === 0 ? undefined : [
|
||||
@@ -264,10 +286,15 @@ export class BlockFeesSubsidyGraphComponent implements OnInit {
|
||||
axisLabel: {
|
||||
color: 'var(--grey)',
|
||||
formatter: (val) => {
|
||||
return `${val} BTC`;
|
||||
return `${val}${this.displayMode === 'percentage' ? '%' : ' BTC'}`;
|
||||
}
|
||||
},
|
||||
min: 0,
|
||||
max: (value) => {
|
||||
if (this.displayMode === 'percentage') {
|
||||
return 100;
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dotted',
|
||||
@@ -295,6 +322,7 @@ export class BlockFeesSubsidyGraphComponent implements OnInit {
|
||||
name: 'Subsidy',
|
||||
yAxisIndex: 0,
|
||||
type: 'bar',
|
||||
barWidth: '90%',
|
||||
stack: 'total',
|
||||
data: this.data.blockSubsidy,
|
||||
},
|
||||
@@ -302,6 +330,7 @@ export class BlockFeesSubsidyGraphComponent implements OnInit {
|
||||
name: 'Fees',
|
||||
yAxisIndex: 0,
|
||||
type: 'bar',
|
||||
barWidth: '90%',
|
||||
stack: 'total',
|
||||
data: this.data.blockFees,
|
||||
},
|
||||
@@ -309,6 +338,7 @@ export class BlockFeesSubsidyGraphComponent implements OnInit {
|
||||
name: 'Subsidy (USD)',
|
||||
yAxisIndex: 1,
|
||||
type: 'bar',
|
||||
barWidth: '90%',
|
||||
stack: 'total',
|
||||
data: this.data.blockSubsidyFiat,
|
||||
},
|
||||
@@ -316,9 +346,26 @@ export class BlockFeesSubsidyGraphComponent implements OnInit {
|
||||
name: 'Fees (USD)',
|
||||
yAxisIndex: 1,
|
||||
type: 'bar',
|
||||
barWidth: '90%',
|
||||
stack: 'total',
|
||||
data: this.data.blockFeesFiat,
|
||||
},
|
||||
{
|
||||
name: 'Subsidy (%)',
|
||||
yAxisIndex: 0,
|
||||
type: 'bar',
|
||||
barWidth: '90%',
|
||||
stack: 'total',
|
||||
data: this.data.blockSubsidyPercent,
|
||||
},
|
||||
{
|
||||
name: 'Fees (%)',
|
||||
yAxisIndex: 0,
|
||||
type: 'bar',
|
||||
barWidth: '90%',
|
||||
stack: 'total',
|
||||
data: this.data.blockFeesPercent,
|
||||
},
|
||||
],
|
||||
dataZoom: this.data.blockFees.length === 0 ? undefined : [{
|
||||
type: 'inside',
|
||||
@@ -349,22 +396,31 @@ export class BlockFeesSubsidyGraphComponent implements OnInit {
|
||||
this.chartInstance = ec;
|
||||
|
||||
this.chartInstance.on('legendselectchanged', (params) => {
|
||||
const isFiat = params.name.includes('USD');
|
||||
if (isFiat === this.showFiat) return;
|
||||
if (this.isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mode: 'normal' | 'fiat' | 'percentage';
|
||||
if (params.name.includes('USD')) {
|
||||
mode = 'fiat';
|
||||
} else if (params.name.includes('%')) {
|
||||
mode = 'percentage';
|
||||
} else {
|
||||
mode = 'normal';
|
||||
}
|
||||
|
||||
if (this.displayMode === mode) return;
|
||||
|
||||
const isActivation = params.selected[params.name];
|
||||
if (isFiat === isActivation) {
|
||||
this.showFiat = true;
|
||||
this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Subsidy' });
|
||||
this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Fees' });
|
||||
this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Subsidy (USD)' });
|
||||
this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Fees (USD)' });
|
||||
} else {
|
||||
this.showFiat = false;
|
||||
this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Subsidy' });
|
||||
this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Fees' });
|
||||
this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Subsidy (USD)' });
|
||||
this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Fees (USD)' });
|
||||
|
||||
if (isActivation) {
|
||||
this.displayMode = mode;
|
||||
this.chartInstance.dispatchAction({ type: this.displayMode === 'normal' ? 'legendSelect' : 'legendUnSelect', name: 'Subsidy' });
|
||||
this.chartInstance.dispatchAction({ type: this.displayMode === 'normal' ? 'legendSelect' : 'legendUnSelect', name: 'Fees' });
|
||||
this.chartInstance.dispatchAction({ type: this.displayMode === 'fiat' ? 'legendSelect' : 'legendUnSelect', name: 'Subsidy (USD)' });
|
||||
this.chartInstance.dispatchAction({ type: this.displayMode === 'fiat' ? 'legendSelect' : 'legendUnSelect', name: 'Fees (USD)' });
|
||||
this.chartInstance.dispatchAction({ type: this.displayMode === 'percentage' ? 'legendSelect' : 'legendUnSelect', name: 'Subsidy (%)' });
|
||||
this.chartInstance.dispatchAction({ type: this.displayMode === 'percentage' ? 'legendSelect' : 'legendUnSelect', name: 'Fees (%)' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -411,6 +467,10 @@ export class BlockFeesSubsidyGraphComponent implements OnInit {
|
||||
return subsidies;
|
||||
}
|
||||
|
||||
subsidyAt(height: number): number {
|
||||
return this.subsidies[Math.floor(Math.min(height / 210000, 33))];
|
||||
}
|
||||
|
||||
onZoom() {
|
||||
const option = this.chartInstance.getOption();
|
||||
const timestamps = option.xAxis[1].data;
|
||||
@@ -432,12 +492,16 @@ export class BlockFeesSubsidyGraphComponent implements OnInit {
|
||||
this.data.blockHeight.splice(startIndex, endIndex - startIndex, ...response.body.map(val => val.avgHeight));
|
||||
this.data.blockFees.splice(startIndex, endIndex - startIndex, ...response.body.map(val => val.avgFees / 100_000_000));
|
||||
this.data.blockFeesFiat.splice(startIndex, endIndex - startIndex, ...response.body.filter(val => val['USD'] > 0).map(val => val.avgFees / 100_000_000 * val['USD']));
|
||||
this.data.blockSubsidy.splice(startIndex, endIndex - startIndex, ...response.body.map(val => this.subsidies[Math.floor(Math.min(val.avgHeight / 210000, 33))] / 100_000_000));
|
||||
this.data.blockSubsidyFiat.splice(startIndex, endIndex - startIndex, ...response.body.filter(val => val['USD'] > 0).map(val => this.subsidies[Math.floor(Math.min(val.avgHeight / 210000, 33))] / 100_000_000 * val['USD']));
|
||||
this.data.blockFeesPercent.splice(startIndex, endIndex - startIndex, ...response.body.map(val => val.avgFees / (val.avgFees + this.subsidyAt(val.avgHeight)) * 100));
|
||||
this.data.blockSubsidy.splice(startIndex, endIndex - startIndex, ...response.body.map(val => this.subsidyAt(val.avgHeight) / 100_000_000));
|
||||
this.data.blockSubsidyFiat.splice(startIndex, endIndex - startIndex, ...response.body.filter(val => val['USD'] > 0).map(val => this.subsidyAt(val.avgHeight) / 100_000_000 * val['USD']));
|
||||
this.data.blockSubsidyPercent.splice(startIndex, endIndex - startIndex, ...response.body.map(val => this.subsidyAt(val.avgHeight) / (val.avgFees + this.subsidyAt(val.avgHeight)) * 100));
|
||||
option.series[0].data = this.data.blockSubsidy;
|
||||
option.series[1].data = this.data.blockFees;
|
||||
option.series[2].data = this.data.blockSubsidyFiat;
|
||||
option.series[3].data = this.data.blockFeesFiat;
|
||||
option.series[4].data = this.data.blockSubsidyPercent;
|
||||
option.series[5].data = this.data.blockFeesPercent;
|
||||
option.xAxis[0].data = this.data.blockHeight;
|
||||
option.xAxis[1].data = this.data.timestamp;
|
||||
this.chartInstance.setOption(option, true);
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
color: var(--fg);
|
||||
font-size: 0.8rem;
|
||||
position: absolute;
|
||||
bottom: 15.8em;
|
||||
bottom: 16.1em;
|
||||
left: 1px;
|
||||
transform: translateX(-50%) rotate(90deg);
|
||||
background: none;
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" [style]="{opacity: isLoading ? 0.5 : 1}">
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null, opacity: isLoading ? 0.5 : 1 }" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
|
||||
@@ -349,7 +349,9 @@ export class HashrateChartComponent implements OnInit {
|
||||
const selectedPowerOfTen: any = selectPowerOfTen(val);
|
||||
const newVal = Math.round(val / selectedPowerOfTen.divider);
|
||||
return `${newVal} ${selectedPowerOfTen.unit}H/s`;
|
||||
}
|
||||
},
|
||||
showMinLabel: false,
|
||||
showMaxLabel: false,
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
@@ -381,7 +383,9 @@ export class HashrateChartComponent implements OnInit {
|
||||
const selectedPowerOfTen: any = selectPowerOfTen(val);
|
||||
const newVal = Math.round(val / selectedPowerOfTen.divider);
|
||||
return `${newVal} ${selectedPowerOfTen.unit}`;
|
||||
}
|
||||
},
|
||||
showMinLabel: false,
|
||||
showMaxLabel: false,
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<!-- Hamburger -->
|
||||
<ng-container *ngIf="servicesEnabled">
|
||||
<div *ngIf="user" class="profile_image_container" [class]="{'anon': !user.imageMd5}" (click)="hamburgerClick($event)">
|
||||
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/images/' + user.username + '?md5=' + user.imageMd5" class="profile_image">
|
||||
<img *ngIf="user.imageMd5" [src]="'/api/v1/services/account/images/' + user.username + '/md5=' + user.imageMd5" class="profile_image">
|
||||
<app-svg-images style="color: lightgrey; fill: lightgray" *ngIf="!user.imageMd5" name="anon"></app-svg-images>
|
||||
</div>
|
||||
<div *ngIf="false && user === null" class="profile_image_container" (click)="hamburgerClick($event)">
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body pl-2 pr-2">
|
||||
<div class="mempool-graph">
|
||||
<app-pool-ranking [height]="graphHeight" [attr.data-cy]="'pool-distribution'" [widget]=true></app-pool-ranking>
|
||||
<app-pool-ranking [height]="poolGraphHeight" [attr.data-cy]="'pool-distribution'" [widget]=true></app-pool-ranking>
|
||||
</div>
|
||||
<div class="mt-1"><a [attr.data-cy]="'pool-distribution-view-more'" [routerLink]="['/graphs/mining/pools' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
|
||||
</div>
|
||||
@@ -41,7 +41,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
|
||||
<div class="fixed-mempool-graph">
|
||||
<app-hashrate-chart [height]="graphHeight" [attr.data-cy]="'hashrate-graph'" [widget]="true"></app-hashrate-chart>
|
||||
<app-hashrate-chart [height]="hashrateGraphHeight" [attr.data-cy]="'hashrate-graph'" [widget]="true"></app-hashrate-chart>
|
||||
</div>
|
||||
<div class="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" fragment="1y" i18n="dashboard.view-more">View more »</a></div>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,8 @@ import { EventType, NavigationStart, Router } from '@angular/router';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MiningDashboardComponent implements OnInit, AfterViewInit {
|
||||
graphHeight = 375;
|
||||
hashrateGraphHeight = 335;
|
||||
poolGraphHeight = 375;
|
||||
|
||||
constructor(
|
||||
private seoService: SeoService,
|
||||
@@ -44,11 +45,14 @@ export class MiningDashboardComponent implements OnInit, AfterViewInit {
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
if (window.innerWidth >= 992) {
|
||||
this.graphHeight = 335;
|
||||
this.hashrateGraphHeight = 335;
|
||||
this.poolGraphHeight = 375;
|
||||
} else if (window.innerWidth >= 768) {
|
||||
this.graphHeight = 245;
|
||||
this.hashrateGraphHeight = 245;
|
||||
this.poolGraphHeight = 265;
|
||||
} else {
|
||||
this.graphHeight = 240;
|
||||
this.hashrateGraphHeight = 240;
|
||||
this.poolGraphHeight = 240;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,8 +76,8 @@
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? '' : 'pb-0'" class="container pb-lg-0">
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" *browserOnly [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" [style]="{opacity: isLoading ? 0.5 : 1}">
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" *browserOnly [style]="{ height: widget ? (height + 'px') : null, opacity: isLoading ? 0.5 : 1 }" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
@media (max-width: 767.98px) {
|
||||
max-height: 230px;
|
||||
}
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { EChartsOption, PieSeriesOption } from '../../graphs/echarts';
|
||||
import { merge, Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { map, shareReplay, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { StorageService } from '../..//services/storage.service';
|
||||
import { MiningService, MiningStats } from '../../services/mining.service';
|
||||
@@ -107,7 +107,7 @@ export class PoolRankingComponent implements OnInit {
|
||||
this.isLoading = false;
|
||||
this.prepareChartOptions(data);
|
||||
}),
|
||||
share()
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { BehaviorSubject, Observable, combineLatest, of, timer } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
|
||||
import { catchError, distinctUntilChanged, filter, map, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended, PoolStat } from '../../interfaces/node-api.interface';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
@@ -28,6 +28,7 @@ export class PoolComponent implements OnInit {
|
||||
gfg = true;
|
||||
|
||||
formatNumber = formatNumber;
|
||||
slugSubscription: Subscription;
|
||||
poolStats$: Observable<PoolStat>;
|
||||
blocks$: Observable<BlockExtended[]>;
|
||||
oobFees$: Observable<AccelerationTotal[]>;
|
||||
@@ -56,38 +57,24 @@ export class PoolComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.poolStats$ = this.route.params.pipe(map((params) => params.slug))
|
||||
this.slugSubscription = this.route.params.pipe(map((params) => params.slug)).subscribe((slug) => {
|
||||
this.isLoading = true;
|
||||
this.blocks = [];
|
||||
this.chartOptions = {};
|
||||
this.slug = slug;
|
||||
this.initializeObservables();
|
||||
});
|
||||
}
|
||||
|
||||
initializeObservables(): void {
|
||||
this.poolStats$ = this.apiService.getPoolHashrate$(this.slug)
|
||||
.pipe(
|
||||
switchMap((slug: any) => {
|
||||
this.isLoading = true;
|
||||
this.slug = slug;
|
||||
return this.apiService.getPoolHashrate$(this.slug)
|
||||
.pipe(
|
||||
switchMap((data) => {
|
||||
this.isLoading = false;
|
||||
const hashrate = data.map(val => [val.timestamp * 1000, val.avgHashrate]);
|
||||
const share = data.map(val => [val.timestamp * 1000, val.share * 100]);
|
||||
this.prepareChartOptions(hashrate, share);
|
||||
return [slug];
|
||||
}),
|
||||
catchError(() => {
|
||||
this.isLoading = false;
|
||||
this.seoService.logSoft404();
|
||||
return of([slug]);
|
||||
})
|
||||
);
|
||||
}),
|
||||
switchMap((slug) => {
|
||||
return this.apiService.getPoolStats$(slug).pipe(
|
||||
catchError(() => {
|
||||
this.isLoading = false;
|
||||
this.seoService.logSoft404();
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
}),
|
||||
tap(() => {
|
||||
this.loadMoreSubject.next(this.blocks[0]?.height);
|
||||
switchMap((data) => {
|
||||
this.isLoading = false;
|
||||
const hashrate = data.map(val => [val.timestamp * 1000, val.avgHashrate]);
|
||||
const share = data.map(val => [val.timestamp * 1000, val.share * 100]);
|
||||
this.prepareChartOptions(hashrate, share);
|
||||
return this.apiService.getPoolStats$(this.slug);
|
||||
}),
|
||||
map((poolStats) => {
|
||||
this.seoService.setTitle(poolStats.pool.name);
|
||||
@@ -101,7 +88,12 @@ export class PoolComponent implements OnInit {
|
||||
return Object.assign({
|
||||
logo: `/resources/mining-pools/` + poolStats.pool.slug + '.svg'
|
||||
}, poolStats);
|
||||
})
|
||||
}),
|
||||
catchError(() => {
|
||||
this.isLoading = false;
|
||||
this.seoService.logSoft404();
|
||||
return of(null);
|
||||
}),
|
||||
);
|
||||
|
||||
this.blocks$ = this.loadMoreSubject
|
||||
@@ -328,4 +320,8 @@ export class PoolComponent implements OnInit {
|
||||
trackByBlock(index: number, block: BlockExtended) {
|
||||
return block.height;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.slugSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { StateService } from '../../services/state.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
import { seoDescriptionNetwork } from '../../shared/common.utils';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-push-transaction',
|
||||
@@ -23,6 +25,9 @@ export class PushTransactionComponent implements OnInit {
|
||||
public stateService: StateService,
|
||||
private seoService: SeoService,
|
||||
private ogService: OpenGraphService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -33,27 +38,100 @@ export class PushTransactionComponent implements OnInit {
|
||||
this.seoService.setTitle($localize`:@@meta.title.push-tx:Broadcast Transaction`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.push-tx:Broadcast a transaction to the ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} network using the transaction's hash.`);
|
||||
this.ogService.setManualOgImage('tx-push.jpg');
|
||||
|
||||
this.route.fragment.subscribe(async (fragment) => {
|
||||
const fragmentParams = new URLSearchParams(fragment || '');
|
||||
return this.handleColdcardPushTx(fragmentParams);
|
||||
});
|
||||
}
|
||||
|
||||
postTx() {
|
||||
async postTx(hex?: string): Promise<string> {
|
||||
this.isLoading = true;
|
||||
this.error = '';
|
||||
this.txId = '';
|
||||
this.apiService.postTransaction$(this.pushTxForm.get('txHash').value)
|
||||
return new Promise((resolve, reject) => {
|
||||
this.apiService.postTransaction$(hex || this.pushTxForm.get('txHash').value)
|
||||
.subscribe((result) => {
|
||||
this.isLoading = false;
|
||||
this.txId = result;
|
||||
this.pushTxForm.reset();
|
||||
resolve(this.txId);
|
||||
},
|
||||
(error) => {
|
||||
if (typeof error.error === 'string') {
|
||||
const matchText = error.error.match('"message":"(.*?)"');
|
||||
this.error = matchText && matchText[1] || error.error;
|
||||
this.error = 'Failed to broadcast transaction, reason: ' + (matchText && matchText[1] || error.error);
|
||||
} else if (error.message) {
|
||||
this.error = error.message;
|
||||
this.error = 'Failed to broadcast transaction, reason: ' + error.message;
|
||||
}
|
||||
this.isLoading = false;
|
||||
reject(this.error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async handleColdcardPushTx(fragmentParams: URLSearchParams): Promise<boolean> {
|
||||
// maybe conforms to Coldcard nfc-pushtx spec
|
||||
if (fragmentParams && fragmentParams.get('t')) {
|
||||
try {
|
||||
const pushNetwork = fragmentParams.get('n');
|
||||
|
||||
// Redirect to the appropriate network-specific URL
|
||||
if (this.stateService.network !== '' && !pushNetwork) {
|
||||
this.router.navigateByUrl(`/pushtx#${fragmentParams.toString()}`);
|
||||
return false;
|
||||
} else if (this.stateService.network !== 'testnet' && pushNetwork === 'XTN') {
|
||||
this.router.navigateByUrl(`/testnet/pushtx#${fragmentParams.toString()}`);
|
||||
return false;
|
||||
} else if (pushNetwork === 'XRT') {
|
||||
this.error = 'Regtest is not supported';
|
||||
return false;
|
||||
} else if (pushNetwork && !['XTN', 'XRT'].includes(pushNetwork)) {
|
||||
this.error = 'Invalid network';
|
||||
return false;
|
||||
}
|
||||
|
||||
const rawTx = this.base64UrlToU8Array(fragmentParams.get('t'));
|
||||
if (!fragmentParams.get('c')) {
|
||||
this.error = 'Missing checksum, URL is probably truncated';
|
||||
return false;
|
||||
}
|
||||
const rawCheck = this.base64UrlToU8Array(fragmentParams.get('c'));
|
||||
|
||||
|
||||
// check checksum
|
||||
const hashTx = await crypto.subtle.digest('SHA-256', rawTx);
|
||||
if (this.u8ArrayToHex(new Uint8Array(hashTx.slice(24))) !== this.u8ArrayToHex(rawCheck)) {
|
||||
this.error = 'Bad checksum, URL is probably truncated';
|
||||
return false;
|
||||
}
|
||||
|
||||
const hexTx = this.u8ArrayToHex(rawTx);
|
||||
this.pushTxForm.get('txHash').setValue(hexTx);
|
||||
|
||||
try {
|
||||
const txid = await this.postTx(hexTx);
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), txid]);
|
||||
} catch (e) {
|
||||
// error already handled
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
this.error = 'Failed to decode transaction';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private base64UrlToU8Array(base64Url: string): Uint8Array {
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/').padEnd(base64Url.length + (4 - base64Url.length % 4) % 4, '=');
|
||||
const binaryString = atob(base64);
|
||||
return new Uint8Array([...binaryString].map(char => char.charCodeAt(0)));
|
||||
}
|
||||
|
||||
private u8ArrayToHex(arr: Uint8Array): string {
|
||||
return Array.from(arr).map(byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export class SearchFormComponent implements OnInit {
|
||||
env: Env;
|
||||
network = '';
|
||||
assets: object = {};
|
||||
pools: object[] = [];
|
||||
isSearching = false;
|
||||
isTypeaheading$ = new BehaviorSubject<boolean>(false);
|
||||
typeAhead$: Observable<any>;
|
||||
@@ -118,7 +119,8 @@ export class SearchFormComponent implements OnInit {
|
||||
if (!text.length) {
|
||||
return of([
|
||||
[],
|
||||
{ nodes: [], channels: [] }
|
||||
{ nodes: [], channels: [] },
|
||||
this.pools
|
||||
]);
|
||||
}
|
||||
this.isTypeaheading$.next(true);
|
||||
@@ -126,6 +128,7 @@ export class SearchFormComponent implements OnInit {
|
||||
return zip(
|
||||
this.electrsApiService.getAddressesByPrefix$(text).pipe(catchError(() => of([]))),
|
||||
[{ nodes: [], channels: [] }],
|
||||
this.getMiningPools()
|
||||
);
|
||||
}
|
||||
return zip(
|
||||
@@ -134,6 +137,7 @@ export class SearchFormComponent implements OnInit {
|
||||
nodes: [],
|
||||
channels: [],
|
||||
}))),
|
||||
this.getMiningPools()
|
||||
);
|
||||
}),
|
||||
map((result: any[]) => {
|
||||
@@ -153,11 +157,14 @@ export class SearchFormComponent implements OnInit {
|
||||
{
|
||||
nodes: [],
|
||||
channels: [],
|
||||
}
|
||||
},
|
||||
this.pools
|
||||
]))
|
||||
]
|
||||
).pipe(
|
||||
map((latestData) => {
|
||||
this.pools = latestData[1][2] || [];
|
||||
|
||||
let searchText = latestData[0];
|
||||
if (!searchText.length) {
|
||||
return {
|
||||
@@ -171,6 +178,7 @@ export class SearchFormComponent implements OnInit {
|
||||
nodes: [],
|
||||
channels: [],
|
||||
liquidAsset: [],
|
||||
pools: []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -187,13 +195,19 @@ export class SearchFormComponent implements OnInit {
|
||||
const matchesTxId = this.regexTransaction.test(searchText) && !this.regexBlockhash.test(searchText);
|
||||
const matchesBlockHash = this.regexBlockhash.test(searchText);
|
||||
const matchesAddress = !matchesTxId && this.regexAddress.test(searchText);
|
||||
const publicKey = matchesAddress && searchText.startsWith('0');
|
||||
const otherNetworks = findOtherNetworks(searchText, this.network as any || 'mainnet', this.env);
|
||||
const liquidAsset = this.assets ? (this.assets[searchText] || []) : [];
|
||||
const pools = this.pools.filter(pool => pool["name"].toLowerCase().includes(searchText.toLowerCase())).slice(0, 10);
|
||||
|
||||
if (matchesDateTime && searchText.indexOf('/') !== -1) {
|
||||
searchText = searchText.replace(/\//g, '-');
|
||||
}
|
||||
|
||||
if (publicKey) {
|
||||
otherNetworks.length = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
searchText: searchText,
|
||||
hashQuickMatch: +(matchesBlockHeight || matchesBlockHash || matchesTxId || matchesAddress || matchesUnixTimestamp || matchesDateTime),
|
||||
@@ -203,11 +217,13 @@ export class SearchFormComponent implements OnInit {
|
||||
txId: matchesTxId,
|
||||
blockHash: matchesBlockHash,
|
||||
address: matchesAddress,
|
||||
publicKey: publicKey,
|
||||
addresses: matchesAddress && addressPrefixSearchResults.length === 1 && searchText === addressPrefixSearchResults[0] ? [] : addressPrefixSearchResults, // If there is only one address and it matches the search text, don't show it in the dropdown
|
||||
otherNetworks: otherNetworks,
|
||||
nodes: lightningResults.nodes,
|
||||
channels: lightningResults.channels,
|
||||
liquidAsset: liquidAsset,
|
||||
pools: pools
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -239,6 +255,8 @@ export class SearchFormComponent implements OnInit {
|
||||
});
|
||||
this.isSearching = false;
|
||||
}
|
||||
} else if (result.slug) {
|
||||
this.navigate('/mining/pool/', result.slug);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,4 +322,29 @@ export class SearchFormComponent implements OnInit {
|
||||
this.isSearching = false;
|
||||
}
|
||||
}
|
||||
|
||||
getMiningPools(): Observable<any> {
|
||||
return this.pools.length ? of(this.pools) : combineLatest([
|
||||
this.apiService.listPools$(undefined),
|
||||
this.apiService.listPools$('1y')
|
||||
]).pipe(
|
||||
map(([poolsResponse, activePoolsResponse]) => {
|
||||
const activePoolSlugs = new Set(activePoolsResponse.body.pools.map(pool => pool.slug));
|
||||
|
||||
return poolsResponse.body.map(pool => ({
|
||||
name: pool.name,
|
||||
slug: pool.slug,
|
||||
active: activePoolSlugs.has(pool.slug)
|
||||
}))
|
||||
// Sort: active pools first, then alphabetically
|
||||
.sort((a, b) => {
|
||||
if (a.active && !b.active) return -1;
|
||||
if (!a.active && b.active) return 1;
|
||||
return a.slug < b.slug ? -1 : 1;
|
||||
});
|
||||
|
||||
}),
|
||||
catchError(() => of([]))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.otherNetworks.length && !results.addresses.length && !results.nodes.length && !results.channels.length && !results.liquidAsset.length">
|
||||
<div class="dropdown-menu show" *ngIf="results" [hidden]="!results.hashQuickMatch && !results.otherNetworks.length && !results.addresses.length && !results.nodes.length && !results.channels.length && !results.liquidAsset.length && !results.pools.length">
|
||||
<ng-template [ngIf]="results.blockHeight">
|
||||
<div class="card-title" i18n="search.bitcoin-block-height">{{ networkName }} Block Height</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
@@ -23,7 +23,7 @@
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : 13 }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.address">
|
||||
<ng-template [ngIf]="results.address && !results.publicKey">
|
||||
<div class="card-title" i18n="search.bitcoin-address">{{ networkName }} Address</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : isMobile ? 17 : 30 }"></ng-container>
|
||||
@@ -35,26 +35,26 @@
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : 13 }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.otherNetworks.length">
|
||||
<div class="card-title danger" i18n="search.other-networks">Other Network Address</div>
|
||||
<ng-template ngFor [ngForOf]="results.otherNetworks" let-otherNetwork let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" [class.inactive]="!otherNetwork.isNetworkAvailable" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: otherNetwork.address| shortenString : isMobile ? 12 : 20 }"></ng-container> <b>({{ otherNetwork.network.charAt(0).toUpperCase() + otherNetwork.network.slice(1) }})</b>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.addresses.length">
|
||||
<div class="card-title" i18n="search.bitcoin-addresses">{{ networkName }} Addresses</div>
|
||||
<ng-template ngFor [ngForOf]="results.addresses" let-address let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + i)" [class.active]="(results.hashQuickMatch + results.otherNetworks.length + i) === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.hashQuickMatch + i)" [class.active]="(results.hashQuickMatch + i) === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="address | shortenString : isMobile ? 25 : 36" [term]="results.searchText"></ngb-highlight>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.pools.length">
|
||||
<div class="card-title" i18n="search.mining-pools">Mining Pools</div>
|
||||
<ng-template ngFor [ngForOf]="results.pools" let-pool let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + i)" [class.active]="results.hashQuickMatch + results.addresses.length + i === activeIdx" [class.inactive]="!pool.active" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="pool.name" [term]="results.searchText"></ngb-highlight>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.nodes.length">
|
||||
<div class="card-title" i18n="search.lightning-nodes">Lightning Nodes</div>
|
||||
<ng-template ngFor [ngForOf]="results.nodes" let-node let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + i)" [class.inactive]="node.status === 0" [class.active]="results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + results.pools.length + i)" [class.inactive]="node.status === 0" [class.active]="results.hashQuickMatch + results.addresses.length + results.pools.length + i === activeIdx" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="node.alias" [term]="results.searchText"></ngb-highlight> <span class="symbol">{{ node.public_key | shortenString : 10 }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
@@ -62,11 +62,25 @@
|
||||
<ng-template [ngIf]="results.channels.length">
|
||||
<div class="card-title" i18n="search.lightning-channels">Lightning Channels</div>
|
||||
<ng-template ngFor [ngForOf]="results.channels" let-channel let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.hashQuickMatch + results.otherNetworks.length + results.addresses.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + results.pools.length + results.nodes.length + i)" [class.inactive]="channel.status === 2" [class.active]="results.hashQuickMatch + results.addresses.length + results.pools.length + results.nodes.length + i === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ngb-highlight [result]="channel.short_id" [term]="results.searchText"></ngb-highlight> <span class="symbol">{{ channel.id }}</span>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.otherNetworks.length">
|
||||
<div class="card-title danger" i18n="search.other-networks">Other Network Address</div>
|
||||
<ng-template ngFor [ngForOf]="results.otherNetworks" let-otherNetwork let-i="index">
|
||||
<button (click)="clickItem(results.hashQuickMatch + results.addresses.length + results.pools.length + results.nodes.length + results.channels.length + i)" [class.active]="(results.hashQuickMatch + results.addresses.length + results.pools.length + results.nodes.length + results.channels.length + i) === activeIdx" [class.inactive]="!otherNetwork.isNetworkAvailable" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: otherNetwork.address | shortenString : isMobile ? 12 : 20 }"></ng-container> <b>({{ otherNetwork.network.charAt(0).toUpperCase() + otherNetwork.network.slice(1) }})</b>
|
||||
</button>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.address && results.publicKey">
|
||||
<div class="card-title" i18n="search.bitcoin-address">{{ networkName }} Address</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
<ng-container *ngTemplateOutlet="goTo; context: { $implicit: results.searchText | shortenString : isMobile ? 17 : 30 }"></ng-container>
|
||||
</button>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="results.liquidAsset.length">
|
||||
<div class="card-title" i18n="search.liquid-asset">Liquid Asset</div>
|
||||
<button (click)="clickItem(0)" [class.active]="0 === activeIdx" type="button" role="option" class="dropdown-item">
|
||||
|
||||
@@ -22,3 +22,7 @@
|
||||
.inactive {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: var(--active-bg);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,11 @@ export class SearchResultsComponent implements OnChanges {
|
||||
ngOnChanges() {
|
||||
this.activeIdx = 0;
|
||||
if (this.results) {
|
||||
this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.otherNetworks, ...this.results.addresses, ...this.results.nodes, ...this.results.channels];
|
||||
this.resultsFlattened = [...(this.results.hashQuickMatch ? [this.results.searchText] : []), ...this.results.addresses, ...this.results.pools, ...this.results.nodes, ...this.results.channels, ...this.results.otherNetworks];
|
||||
// If searchText is a public key corresponding to a node, select it by default
|
||||
if (this.results.publicKey && this.results.nodes.length > 0) {
|
||||
this.activeIdx = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -347,6 +347,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (txPosition.position?.accelerated) {
|
||||
this.tx.acceleration = true;
|
||||
this.tx.acceleratedBy = txPosition.position?.acceleratedBy;
|
||||
}
|
||||
|
||||
if (txPosition.position?.block === 0) {
|
||||
@@ -602,6 +603,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
if (cpfpInfo.acceleration) {
|
||||
this.tx.acceleration = cpfpInfo.acceleration;
|
||||
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
|
||||
}
|
||||
|
||||
this.cpfpInfo = cpfpInfo;
|
||||
|
||||
@@ -419,7 +419,11 @@
|
||||
<ng-template #detailsRight>
|
||||
<ng-container *ngTemplateOutlet="feeRow"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="feeRateRow"></ng-container>
|
||||
<ng-container *ngTemplateOutlet="effectiveRateRow"></ng-container>
|
||||
@if (!isLoadingTx && !tx?.status?.confirmed && isAcceleration && ((cpfpInfo && hasEffectiveFeeRate) || accelerationInfo)) {
|
||||
<ng-container *ngTemplateOutlet="acceleratingRow"></ng-container>
|
||||
} @else {
|
||||
<ng-container *ngTemplateOutlet="effectiveRateRow"></ng-container>
|
||||
}
|
||||
@if (tx?.status?.confirmed) {
|
||||
<ng-container *ngTemplateOutlet="minerRow"></ng-container>
|
||||
}
|
||||
@@ -638,6 +642,15 @@
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #acceleratingRow>
|
||||
<tr>
|
||||
<td rowspan="2" colspan="2" style="padding: 0;">
|
||||
<app-active-acceleration-box [tx]="tx" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats"></app-active-acceleration-box>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #minerRow>
|
||||
@if (network === '') {
|
||||
@if (!isLoadingTx) {
|
||||
|
||||
@@ -32,6 +32,7 @@ import { isFeatureActive } from '../../bitcoin.utils';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { ZONE_SERVICE } from '../../injection-tokens';
|
||||
import { MiningService, MiningStats } from '../../services/mining.service';
|
||||
|
||||
interface Pool {
|
||||
id: number;
|
||||
@@ -98,6 +99,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
isAcceleration: boolean = false;
|
||||
filters: Filter[] = [];
|
||||
showCpfpDetails = false;
|
||||
miningStats: MiningStats;
|
||||
fetchCpfp$ = new Subject<string>();
|
||||
fetchRbfHistory$ = new Subject<string>();
|
||||
fetchCachedTx$ = new Subject<string>();
|
||||
@@ -151,6 +153,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
private priceService: PriceService,
|
||||
private storageService: StorageService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private miningService: MiningService,
|
||||
private cd: ChangeDetectorRef,
|
||||
@Inject(ZONE_SERVICE) private zoneService: any,
|
||||
) {}
|
||||
@@ -377,7 +380,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
this.txInBlockIndex = this.mempoolPosition.block;
|
||||
|
||||
if (txPosition.cpfp !== undefined) {
|
||||
if (txPosition.position.acceleratedBy) {
|
||||
txPosition.cpfp.acceleratedBy = txPosition.position.acceleratedBy;
|
||||
}
|
||||
this.setCpfpInfo(txPosition.cpfp);
|
||||
} else if ((this.tx?.acceleration && txPosition.position.acceleratedBy)) {
|
||||
this.tx.acceleratedBy = txPosition.position.acceleratedBy;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -696,6 +704,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
if (cpfpInfo.acceleration) {
|
||||
this.tx.acceleration = cpfpInfo.acceleration;
|
||||
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
|
||||
this.setIsAccelerated(firstCpfp);
|
||||
}
|
||||
|
||||
@@ -709,10 +718,16 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
setIsAccelerated(initialState: boolean = false) {
|
||||
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool === this.pool.id || pool?.['pool_unique_id'] === this.pool.id))));
|
||||
this.isAcceleration = (this.tx.acceleration || (this.accelerationInfo && this.pool && this.accelerationInfo.pools.some(pool => (pool == this.pool.id || pool?.['pool_unique_id'] == this.pool.id))));
|
||||
if (this.isAcceleration && initialState) {
|
||||
this.showAccelerationSummary = false;
|
||||
}
|
||||
if (this.isAcceleration) {
|
||||
// this immediately returns cached stats if we fetched them recently
|
||||
this.miningService.getMiningStats('1w').subscribe(stats => {
|
||||
this.miningStats = stats;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setFeatures(): void {
|
||||
@@ -790,6 +805,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1);
|
||||
}
|
||||
|
||||
getUnacceleratedFeeRate(tx: Transaction, accelerated: boolean): number {
|
||||
if (accelerated) {
|
||||
let ancestorVsize = tx.weight / 4;
|
||||
let ancestorFee = tx.fee;
|
||||
for (const ancestor of tx.ancestors || []) {
|
||||
ancestorVsize += (ancestor.weight / 4);
|
||||
ancestorFee += ancestor.fee;
|
||||
}
|
||||
return Math.min(tx.fee / (tx.weight / 4), (ancestorFee / ancestorVsize));
|
||||
} else {
|
||||
return tx.effectiveFeePerVsize;
|
||||
}
|
||||
}
|
||||
|
||||
setupGraph() {
|
||||
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1));
|
||||
this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Routes, RouterModule } from '@angular/router';
|
||||
import { TransactionComponent } from './transaction.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module';
|
||||
import { GraphsModule } from '../../graphs/graphs.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -30,6 +31,7 @@ export class TransactionRoutingModule { }
|
||||
CommonModule,
|
||||
TransactionRoutingModule,
|
||||
SharedModule,
|
||||
GraphsModule,
|
||||
TxBowtieModule,
|
||||
],
|
||||
declarations: [
|
||||
|
||||
@@ -36,6 +36,7 @@ import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools
|
||||
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
|
||||
import { AddressComponent } from '../components/address/address.component';
|
||||
import { AddressGraphComponent } from '../components/address-graph/address-graph.component';
|
||||
import { ActiveAccelerationBox } from '../components/acceleration/active-acceleration-box/active-acceleration-box.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@NgModule({
|
||||
@@ -75,6 +76,7 @@ import { CommonModule } from '@angular/common';
|
||||
HashrateChartPoolsComponent,
|
||||
BlockHealthGraphComponent,
|
||||
AddressGraphComponent,
|
||||
ActiveAccelerationBox,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -86,6 +88,7 @@ import { CommonModule } from '@angular/common';
|
||||
],
|
||||
exports: [
|
||||
NgxEchartsModule,
|
||||
ActiveAccelerationBox,
|
||||
]
|
||||
})
|
||||
export class GraphsModule { }
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface Transaction {
|
||||
bestDescendant?: BestDescendant | null;
|
||||
cpfpChecked?: boolean;
|
||||
acceleration?: boolean;
|
||||
acceleratedBy?: number[];
|
||||
deleteAfter?: number;
|
||||
_unblinded?: any;
|
||||
_deduced?: boolean;
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface CpfpInfo {
|
||||
sigops?: number;
|
||||
adjustedVsize?: number;
|
||||
acceleration?: boolean;
|
||||
acceleratedBy?: number[];
|
||||
}
|
||||
|
||||
export interface RbfInfo {
|
||||
@@ -132,6 +133,7 @@ export interface ITranslators { [language: string]: string; }
|
||||
*/
|
||||
export interface SinglePoolStats {
|
||||
poolId: number;
|
||||
poolUniqueId: number; // unique global pool id
|
||||
name: string;
|
||||
link: string;
|
||||
blockCount: number;
|
||||
@@ -245,7 +247,8 @@ export interface RbfTransaction extends TransactionStripped {
|
||||
export interface MempoolPosition {
|
||||
block: number,
|
||||
vsize: number,
|
||||
accelerated?: boolean
|
||||
accelerated?: boolean,
|
||||
acceleratedBy?: number[],
|
||||
}
|
||||
|
||||
export interface RewardStats {
|
||||
|
||||
@@ -80,6 +80,12 @@ export class GroupComponent implements OnInit {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
nodes.map((node) => {
|
||||
node.channels = node.opened_channel_count;
|
||||
return node;
|
||||
});
|
||||
|
||||
const sumLiquidity = nodes.reduce((partialSum, a) => partialSum + parseInt(a.capacity, 10), 0);
|
||||
const sumChannels = nodes.reduce((partialSum, a) => partialSum + a.opened_channel_count, 0);
|
||||
|
||||
|
||||
@@ -32,6 +32,10 @@ const routes: Routes = [
|
||||
path: 'tx/push',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'pushtx',
|
||||
component: PushTransactionComponent,
|
||||
},
|
||||
{
|
||||
path: 'tx/test',
|
||||
component: TestTransactionsComponent,
|
||||
|
||||
@@ -253,8 +253,9 @@ export class ApiService {
|
||||
)
|
||||
.pipe(
|
||||
map((response) => {
|
||||
response.body.pools.forEach((pool) => {
|
||||
if (pool.poolUniqueId === 0) {
|
||||
const pools = interval !== undefined ? response.body.pools : response.body;
|
||||
pools.forEach((pool) => {
|
||||
if ((interval !== undefined && pool.poolUniqueId === 0) || (interval === undefined && pool.unique_id === 0)) {
|
||||
pool.name = $localize`:@@e5d8bb389c702588877f039d72178f219453a72d:Unknown`;
|
||||
}
|
||||
});
|
||||
@@ -402,9 +403,13 @@ export class ApiService {
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/txids/', { params });
|
||||
}
|
||||
|
||||
lightningSearch$(searchText: string): Observable<any[]> {
|
||||
lightningSearch$(searchText: string): Observable<{ nodes: any[], channels: any[] }> {
|
||||
let params = new HttpParams().set('searchText', searchText);
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/search', { params });
|
||||
// Don't request the backend if searchText is less than 3 characters
|
||||
if (searchText.length < 3) {
|
||||
return of({ nodes: [], channels: [] });
|
||||
}
|
||||
return this.httpClient.get<{ nodes: any[], channels: any[] }>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/search', { params });
|
||||
}
|
||||
|
||||
getNodesPerIsp(): Observable<any> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { PoolsStats, SinglePoolStats } from '../interfaces/node-api.interface';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { StateService } from './state.service';
|
||||
@@ -25,6 +25,12 @@ export interface MiningStats {
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MiningService {
|
||||
cache: {
|
||||
[interval: string]: {
|
||||
lastUpdated: number;
|
||||
data: MiningStats;
|
||||
}
|
||||
} = {};
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
@@ -36,9 +42,20 @@ export class MiningService {
|
||||
* Generate pool ranking stats
|
||||
*/
|
||||
public getMiningStats(interval: string): Observable<MiningStats> {
|
||||
return this.apiService.listPools$(interval).pipe(
|
||||
map(response => this.generateMiningStats(response))
|
||||
);
|
||||
// returned cached data fetched within the last 5 minutes
|
||||
if (this.cache[interval] && this.cache[interval].lastUpdated > (Date.now() - (5 * 60000))) {
|
||||
return of(this.cache[interval].data);
|
||||
} else {
|
||||
return this.apiService.listPools$(interval).pipe(
|
||||
map(response => this.generateMiningStats(response)),
|
||||
tap(stats => {
|
||||
this.cache[interval] = {
|
||||
lastUpdated: Date.now(),
|
||||
data: stats,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 340 KiB After Width: | Height: | Size: 66 KiB |
@@ -1,13 +1,12 @@
|
||||
# proxy cache
|
||||
proxy_cache_path /var/cache/nginx/services keys_zone=services:20m levels=1:2 inactive=30d max_size=200m;
|
||||
proxy_cache_path /var/cache/nginx/apihot keys_zone=apihot:20m levels=1:2 inactive=60m max_size=20m;
|
||||
proxy_cache_path /var/cache/nginx/apiwarm keys_zone=apiwarm:20m levels=1:2 inactive=24h max_size=200m;
|
||||
proxy_cache_path /var/cache/nginx/services keys_zone=services:200m levels=1:2 inactive=30d max_size=200m;
|
||||
proxy_cache_path /var/cache/nginx/apihot keys_zone=apihot:200m levels=1:2 inactive=60m max_size=20m;
|
||||
proxy_cache_path /var/cache/nginx/apiwarm keys_zone=apiwarm:200m levels=1:2 inactive=24h max_size=200m;
|
||||
proxy_cache_path /var/cache/nginx/apinormal keys_zone=apinormal:200m levels=1:2 inactive=30d max_size=2000m;
|
||||
proxy_cache_path /var/cache/nginx/apicold keys_zone=apicold:200m levels=1:2 inactive=365d max_size=2000m;
|
||||
proxy_cache_path /var/cache/nginx/apicold keys_zone=apicold:200m levels=1:2 inactive=60d max_size=2000m;
|
||||
|
||||
proxy_cache_path /var/cache/nginx/unfurler keys_zone=unfurler:200m levels=1:2 inactive=30d max_size=2000m;
|
||||
proxy_cache_path /var/cache/nginx/slurper keys_zone=slurper:500m levels=1:2 inactive=365d max_size=5000m;
|
||||
proxy_cache_path /var/cache/nginx/markets keys_zone=markets:20m levels=1:2 inactive=365d max_size=100m;
|
||||
proxy_cache_path /var/cache/nginx/slurper keys_zone=slurper:500m levels=1:2 inactive=60d max_size=5000m;
|
||||
|
||||
types_hash_max_size 8192;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user