Compare commits

...

50 Commits

Author SHA1 Message Date
Mononaut
2a4ab30c95 Relax pool id equality 2024-06-05 21:58:33 +00:00
wiz
9b9aaed757 Merge pull request #5132 from mempool/mononaut/coldcard-nfc
Experimental auto-push URL support
2024-06-04 12:04:25 +09:00
Mononaut
b699063153 Experimental auto-push URL support 2024-06-03 21:45:36 +00:00
wiz
6947e19ca9 ops: Tweak nginx cache config 2024-06-03 18:21:14 +09:00
softsimon
a0d3afb4d2 Merge pull request #5124 from mempool/natsoni/fix-lightning-search
Searchbar: wait for 3 characters before requesting lightning data
2024-06-01 14:22:19 +07:00
softsimon
67afda7dcf Merge branch 'master' into natsoni/fix-lightning-search 2024-06-01 14:20:00 +07:00
softsimon
a56af00500 Merge pull request #5123 from mempool/natsoni/search-results-ordering
Improve search results ordering
2024-06-01 14:19:48 +07:00
softsimon
e3971af207 Merge pull request #5122 from mempool/natsoni/fix-pool-ranking
Fix pool ranking table
2024-06-01 14:17:50 +07:00
natsoni
f17635193a Fix pool ranking component update 2024-05-31 17:25:36 +02:00
softsimon
1c73dc59f9 Merge branch 'master' into natsoni/search-results-ordering 2024-05-31 22:18:43 +07:00
softsimon
3adbba2959 Merge branch 'master' into natsoni/fix-lightning-search 2024-05-31 21:20:31 +07:00
softsimon
ea1629fba8 Merge pull request #5121 from mempool/dependabot/npm_and_yarn/backend/mysql2-3.10.0
Bump mysql2 from 3.9.7 to 3.10.0 in /backend
2024-05-31 21:20:02 +07:00
softsimon
87a4c087e5 Merge pull request #5118 from mempool/natsoni/fix-pool-page-update
Fix pool page update
2024-05-31 21:19:35 +07:00
softsimon
692edea1ce Merge branch 'master' into natsoni/fix-pool-page-update 2024-05-31 21:17:09 +07:00
softsimon
11cfb8a783 Merge pull request #5117 from mempool/natsoni/pools-search
Add mining pools to search results
2024-05-31 21:16:46 +07:00
natsoni
0b953f21b0 Only query lightning search if more than 3 characters 2024-05-31 15:40:27 +02:00
natsoni
d5508872dd Select lightning node by default in search results of public key 2024-05-31 15:08:58 +02:00
natsoni
321181d708 Update search results ordering 2024-05-31 13:52:37 +02:00
natsoni
f3bd50d4ab Revert "Update search results ordering"
This reverts commit 00838ea947.
2024-05-31 13:37:30 +02:00
dependabot[bot]
12a843c386 Bump mysql2 from 3.9.7 to 3.10.0 in /backend
Bumps [mysql2](https://github.com/sidorares/node-mysql2) from 3.9.7 to 3.10.0.
- [Release notes](https://github.com/sidorares/node-mysql2/releases)
- [Changelog](https://github.com/sidorares/node-mysql2/blob/master/Changelog.md)
- [Commits](https://github.com/sidorares/node-mysql2/compare/v3.9.7...v3.10.0)

---
updated-dependencies:
- dependency-name: mysql2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-31 02:34:09 +00:00
natsoni
21f91bcb6e Fix pool page update on slug change 2024-05-30 17:58:28 +02:00
natsoni
d57bd56743 Use includes() instead of startsWith() to search for pool names 2024-05-30 15:08:20 +02:00
natsoni
08969592ea Fix i18n for unknown pool search 2024-05-30 14:46:48 +02:00
wiz
f0437886ee Merge pull request #5116 from mempool/simon/fix-undefined-group-channels 2024-05-30 18:29:53 +09:00
softsimon
cfedb5fd24 Fix for undefined LN group channels 2024-05-30 16:15:51 +07:00
wiz
a9ad892495 Merge pull request #5112 from mempool/mononaut/polish-acc-pie
Polish acceleration pie chart section
2024-05-30 17:58:58 +09:00
natsoni
00838ea947 Update search results ordering 2024-05-30 10:34:40 +02:00
natsoni
7761ea53c6 Add mining pools to search bar 2024-05-30 09:31:44 +02:00
softsimon
aeeb4af9ba Merge pull request #5110 from mempool/natsoni/lift-up-blockchain-toggle
Slightly lift up blockchain toggle button
2024-05-29 16:21:45 +07:00
softsimon
9186f664da Merge pull request #5109 from mempool/natsoni/fix-mining-graphs
Fix widget mining graphs
2024-05-29 15:58:56 +07:00
softsimon
83db2a3b72 Add margin to graph on pool ranking page 2024-05-29 15:58:39 +07:00
natsoni
3cfd54b4c5 Update mining dashboard graph heights 2024-05-29 10:27:45 +02:00
Mononaut
c6db016c99 Show hashrate pie chart immediately on acceleration 2024-05-28 21:33:09 +00:00
Mononaut
6f6a9ea1a4 Brighter purple pie chart 2024-05-28 21:07:36 +00:00
Mononaut
83246be962 Responsive active acceleration details 2024-05-28 21:06:58 +00:00
natsoni
dcd94d868a Slightly lift up blockchain toggle button 2024-05-28 16:11:48 +02:00
natsoni
e9fc5c0433 Fix widget mining graphs 2024-05-28 16:11:06 +02:00
wiz
e281684ca4 Merge pull request #5107 from mempool/mononaut/acceleration-piechart-hotfix
Hotfix for acceleration pie chart section logic
2024-05-28 12:37:22 +09:00
Mononaut
6a915c0b88 Hotfix for acceleration pie chart section logic 2024-05-28 03:35:41 +00:00
wiz
078dc8d9a1 Merge pull request #5090 from mempool/mononaut/update-onbtc-preview-img
Update onbtc preview fallback image
2024-05-28 11:26:33 +09:00
wiz
232f81b906 Merge pull request #5017 from mempool/nymkappa/image-md5
[account] update profile image md5
2024-05-28 11:25:55 +09:00
wiz
8701119304 Merge pull request #5101 from mempool/natsoni/block-rewards-graph
Fees vs subsidy graph: add percentage mode
2024-05-28 11:23:57 +09:00
wiz
33c9f4a8dc Merge pull request #5103 from mempool/mononaut/multi-pool-acc
inline acceleration hashrate pie chart
2024-05-28 11:23:25 +09:00
natsoni
0654872627 Fix graph legend update while load bug and remove unnecessary query 2024-05-27 16:49:29 +02:00
natsoni
cca798eeaa Remove unnecessary filters in graph 2024-05-27 16:42:17 +02:00
Mononaut
1498db3b33 Backend support for multi-pool acceleration details 2024-05-26 20:47:36 +00:00
Mononaut
05b022dec8 multi-pool active accelerating details component 2024-05-26 20:39:35 +00:00
natsoni
6c6c18830c Fees vs subsidy graph: add percentage mode 2024-05-25 12:32:38 +02:00
Mononaut
69786d5b4b Update onbtc preview fallback image 2024-05-20 23:48:53 +00:00
nymkappa
8b1acbe13b [account] update profile image md5 2024-04-27 14:49:06 +02:00
40 changed files with 658 additions and 135 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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,
};
}
}

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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>

View File

@@ -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,

View File

@@ -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)">

View File

@@ -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 &raquo;</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 &raquo;</a></div>
</div>

View File

@@ -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;
}
}
}

View File

@@ -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">

View File

@@ -24,6 +24,7 @@
@media (max-width: 767.98px) {
max-height: 230px;
}
margin-bottom: 20px;
}
.chart-widget {
width: 100%;

View File

@@ -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)
);
}

View File

@@ -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();
}
}

View File

@@ -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('');
}
}

View File

@@ -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([]))
);
}
}

View File

@@ -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>&nbsp;<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> &nbsp;<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> &nbsp;<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>&nbsp;<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">

View File

@@ -22,3 +22,7 @@
.inactive {
opacity: 0.2;
}
.active {
background-color: var(--active-bg);
}

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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: [

View File

@@ -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 { }

View File

@@ -20,6 +20,7 @@ export interface Transaction {
bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean;
acceleration?: boolean;
acceleratedBy?: number[];
deleteAfter?: number;
_unblinded?: any;
_deduced?: boolean;

View File

@@ -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 {

View File

@@ -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);

View File

@@ -32,6 +32,10 @@ const routes: Routes = [
path: 'tx/push',
component: PushTransactionComponent,
},
{
path: 'pushtx',
component: PushTransactionComponent,
},
{
path: 'tx/test',
component: TestTransactionsComponent,

View File

@@ -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> {

View File

@@ -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

View File

@@ -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;