Merge branch 'master' into nymkappa/tx-overflow
This commit is contained in:
@@ -281,3 +281,15 @@ export function isFeatureActive(network: string, height: number, feature: 'rbf'
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function calcScriptHash$(script: string): Promise<string> {
|
||||
if (!/^[0-9a-fA-F]*$/.test(script) || script.length % 2 !== 0) {
|
||||
throw new Error('script is not a valid hex string');
|
||||
}
|
||||
const buf = Uint8Array.from(script.match(/.{2}/g).map((byte) => parseInt(byte, 16)));
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', buf);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray
|
||||
.map((bytes) => bytes.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
@@ -411,7 +411,7 @@
|
||||
Trademark Notice<br>
|
||||
</div>
|
||||
<p>
|
||||
The Mempool Open Source Project™, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
The Mempool Open Source Project®, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
</p>
|
||||
<p>
|
||||
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||
|
||||
@@ -64,13 +64,15 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
||||
this.address = null;
|
||||
this.addressInfo = null;
|
||||
this.addressString = params.get('id') || '';
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) {
|
||||
this.addressString = this.addressString.toLowerCase();
|
||||
}
|
||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
|
||||
return this.electrsApiService.getAddress$(this.addressString)
|
||||
.pipe(
|
||||
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
|
||||
? this.electrsApiService.getPubKeyAddress$(this.addressString)
|
||||
: this.electrsApiService.getAddress$(this.addressString)
|
||||
).pipe(
|
||||
catchError((err) => {
|
||||
this.isLoadingAddress = false;
|
||||
this.error = err;
|
||||
|
||||
@@ -81,6 +81,7 @@ h1 {
|
||||
top: 11px;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
max-width: calc(100% - 180px);
|
||||
top: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
|
||||
import { Address, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { Address, ScriptHash, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
@@ -72,7 +72,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.addressInfo = null;
|
||||
document.body.scrollTo(0, 0);
|
||||
this.addressString = params.get('id') || '';
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
|
||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(this.addressString)) {
|
||||
this.addressString = this.addressString.toLowerCase();
|
||||
}
|
||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||
@@ -83,8 +83,11 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
.pipe(filter((state) => state === 2 && this.transactions && this.transactions.length > 0))
|
||||
)
|
||||
.pipe(
|
||||
switchMap(() => this.electrsApiService.getAddress$(this.addressString)
|
||||
.pipe(
|
||||
switchMap(() => (
|
||||
this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
|
||||
? this.electrsApiService.getPubKeyAddress$(this.addressString)
|
||||
: this.electrsApiService.getAddress$(this.addressString)
|
||||
).pipe(
|
||||
catchError((err) => {
|
||||
this.isLoadingAddress = false;
|
||||
this.error = err;
|
||||
@@ -114,7 +117,9 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.updateChainStats();
|
||||
this.isLoadingAddress = false;
|
||||
this.isLoadingTransactions = true;
|
||||
return this.electrsApiService.getAddressTransactions$(address.address);
|
||||
return address.is_pubkey
|
||||
? this.electrsApiService.getScriptHashTransactions$((address.address.length === 66 ? '21' : '41') + address.address + 'ac')
|
||||
: this.electrsApiService.getAddressTransactions$(address.address);
|
||||
}),
|
||||
switchMap((transactions) => {
|
||||
this.tempTransactions = transactions;
|
||||
@@ -161,31 +166,8 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
|
||||
this.stateService.mempoolTransactions$
|
||||
.subscribe((transaction) => {
|
||||
if (this.transactions.some((t) => t.txid === transaction.txid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.transactions.unshift(transaction);
|
||||
this.transactions = this.transactions.slice();
|
||||
this.txCount++;
|
||||
|
||||
if (transaction.vout.some((vout) => vout.scriptpubkey_address === this.address.address)) {
|
||||
this.audioService.playSound('cha-ching');
|
||||
} else {
|
||||
this.audioService.playSound('chime');
|
||||
}
|
||||
|
||||
transaction.vin.forEach((vin) => {
|
||||
if (vin.prevout.scriptpubkey_address === this.address.address) {
|
||||
this.sent += vin.prevout.value;
|
||||
}
|
||||
});
|
||||
transaction.vout.forEach((vout) => {
|
||||
if (vout.scriptpubkey_address === this.address.address) {
|
||||
this.received += vout.value;
|
||||
}
|
||||
});
|
||||
.subscribe(tx => {
|
||||
this.addTransaction(tx);
|
||||
});
|
||||
|
||||
this.stateService.blockTransactions$
|
||||
@@ -195,12 +177,47 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
tx.status = transaction.status;
|
||||
this.transactions = this.transactions.slice();
|
||||
this.audioService.playSound('magic');
|
||||
} else {
|
||||
if (this.addTransaction(transaction, false)) {
|
||||
this.audioService.playSound('magic');
|
||||
}
|
||||
}
|
||||
this.totalConfirmedTxCount++;
|
||||
this.loadedConfirmedTxCount++;
|
||||
});
|
||||
}
|
||||
|
||||
addTransaction(transaction: Transaction, playSound: boolean = true): boolean {
|
||||
if (this.transactions.some((t) => t.txid === transaction.txid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.transactions.unshift(transaction);
|
||||
this.transactions = this.transactions.slice();
|
||||
this.txCount++;
|
||||
|
||||
if (playSound) {
|
||||
if (transaction.vout.some((vout) => vout?.scriptpubkey_address === this.address.address)) {
|
||||
this.audioService.playSound('cha-ching');
|
||||
} else {
|
||||
this.audioService.playSound('chime');
|
||||
}
|
||||
}
|
||||
|
||||
transaction.vin.forEach((vin) => {
|
||||
if (vin?.prevout?.scriptpubkey_address === this.address.address) {
|
||||
this.sent += vin.prevout.value;
|
||||
}
|
||||
});
|
||||
transaction.vout.forEach((vout) => {
|
||||
if (vout?.scriptpubkey_address === this.address.address) {
|
||||
this.received += vout.value;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (this.isLoadingTransactions || !this.totalConfirmedTxCount || this.loadedConfirmedTxCount >= this.totalConfirmedTxCount) {
|
||||
return;
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export default class TxView implements TransactionStripped {
|
||||
value: number;
|
||||
feerate: number;
|
||||
rate?: number;
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
|
||||
context?: 'projected' | 'actual';
|
||||
scene?: BlockScene;
|
||||
|
||||
@@ -207,7 +207,7 @@ export default class TxView implements TransactionStripped {
|
||||
return auditColors.censored;
|
||||
case 'missing':
|
||||
case 'sigop':
|
||||
case 'fullrbf':
|
||||
case 'rbf':
|
||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||
case 'fresh':
|
||||
case 'freshcpfp':
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<td *ngSwitchCase="'freshcpfp'"><span class="badge badge-warning" i18n="transaction.audit.recently-cpfped">Recently CPFP'd</span></td>
|
||||
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
||||
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||
<td *ngSwitchCase="'fullrbf'"><span class="badge badge-warning" i18n="transaction.audit.fullrbf">Full RBF</span></td>
|
||||
<td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td>
|
||||
</ng-container>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -144,10 +144,12 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
for (const block of blocks) {
|
||||
if (block.id === this.blockHash) {
|
||||
this.block = block;
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
if (block?.extras?.reward != undefined) {
|
||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||
if (block.extras) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
if (block?.extras?.reward != undefined) {
|
||||
this.fees = block.extras.reward / 100000000 - this.blockSubsidy;
|
||||
}
|
||||
}
|
||||
} else if (block.height === this.block?.height) {
|
||||
this.block.stale = true;
|
||||
@@ -246,8 +248,10 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.updateAuditAvailableFromBlockHeight(block.height);
|
||||
this.block = block;
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
if (block.extras) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
}
|
||||
this.blockHeight = block.height;
|
||||
this.lastBlockHeight = this.blockHeight;
|
||||
this.nextBlockHeight = block.height + 1;
|
||||
@@ -335,7 +339,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
const isSelected = {};
|
||||
const isFresh = {};
|
||||
const isSigop = {};
|
||||
const isFullRbf = {};
|
||||
const isRbf = {};
|
||||
this.numMissing = 0;
|
||||
this.numUnexpected = 0;
|
||||
|
||||
@@ -359,7 +363,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
isSigop[txid] = true;
|
||||
}
|
||||
for (const txid of blockAudit.fullrbfTxs || []) {
|
||||
isFullRbf[txid] = true;
|
||||
isRbf[txid] = true;
|
||||
}
|
||||
// set transaction statuses
|
||||
for (const tx of blockAudit.template) {
|
||||
@@ -377,8 +381,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
} else if (isSigop[tx.txid]) {
|
||||
tx.status = 'sigop';
|
||||
} else if (isFullRbf[tx.txid]) {
|
||||
tx.status = 'fullrbf';
|
||||
} else if (isRbf[tx.txid]) {
|
||||
tx.status = 'rbf';
|
||||
} else {
|
||||
tx.status = 'missing';
|
||||
}
|
||||
@@ -394,8 +398,8 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
tx.status = 'added';
|
||||
} else if (inTemplate[tx.txid]) {
|
||||
tx.status = 'found';
|
||||
} else if (isFullRbf[tx.txid]) {
|
||||
tx.status = 'fullrbf';
|
||||
} else if (isRbf[tx.txid]) {
|
||||
tx.status = 'rbf';
|
||||
} else {
|
||||
tx.status = 'selected';
|
||||
isSelected[tx.txid] = true;
|
||||
|
||||
@@ -113,8 +113,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
const animate = this.chainTip != null && latestHeight > this.chainTip;
|
||||
|
||||
for (const block of blocks) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
if (block?.extras) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
}
|
||||
}
|
||||
|
||||
this.blocks = blocks;
|
||||
@@ -251,7 +253,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (height >= 0) {
|
||||
this.cacheService.loadBlock(height);
|
||||
block = this.cacheService.getCachedBlock(height) || null;
|
||||
if (block) {
|
||||
if (block?.extras) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
}
|
||||
@@ -293,8 +295,10 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
onBlockLoaded(block: BlockExtended) {
|
||||
const blockIndex = this.height - block.height;
|
||||
if (blockIndex >= 0 && blockIndex < this.blocks.length) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
if (block?.extras) {
|
||||
block.extras.minFee = this.getMinBlockFee(block);
|
||||
block.extras.maxFee = this.getMaxBlockFee(block);
|
||||
}
|
||||
this.blocks[blockIndex] = block;
|
||||
this.blockStyles[blockIndex] = this.getStyleForBlock(block, blockIndex);
|
||||
}
|
||||
|
||||
@@ -82,9 +82,7 @@ export class BlockchainComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
this.mempoolOffset = Math.max(0, width - this.dividerOffset);
|
||||
this.cd.markForCheck();
|
||||
setTimeout(() => {
|
||||
this.mempoolOffsetChange.emit(this.mempoolOffset);
|
||||
}, 0);
|
||||
this.mempoolOffsetChange.emit(this.mempoolOffset);
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
|
||||
@@ -68,7 +68,7 @@ export class BlocksList implements OnInit {
|
||||
for (const block of blocks) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
block.extras.pool.logo = `/resources/mining-pools/` +
|
||||
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
||||
block.extras.pool.slug + '.svg';
|
||||
}
|
||||
}
|
||||
if (this.widget) {
|
||||
@@ -84,10 +84,10 @@ export class BlocksList implements OnInit {
|
||||
.pipe(
|
||||
switchMap((blocks) => {
|
||||
if (blocks[0].height <= this.lastBlockHeight) {
|
||||
return [null]; // Return an empty stream so the last pipe is not executed
|
||||
return of([]); // Return an empty stream so the last pipe is not executed
|
||||
}
|
||||
this.lastBlockHeight = blocks[0].height;
|
||||
return blocks;
|
||||
return of(blocks);
|
||||
})
|
||||
)
|
||||
])
|
||||
@@ -102,7 +102,7 @@ export class BlocksList implements OnInit {
|
||||
if (this.stateService.env.MINING_DASHBOARD) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
blocks[1][0].extras.pool.logo = `/resources/mining-pools/` +
|
||||
blocks[1][0].extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
||||
blocks[1][0].extras.pool.slug + '.svg';
|
||||
}
|
||||
acc.unshift(blocks[1][0]);
|
||||
acc = acc.slice(0, this.widget ? 6 : 15);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { combineLatest, Observable, timer } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
import { StateService } from '../..//services/state.service';
|
||||
@@ -61,6 +61,7 @@ export class DifficultyComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) { }
|
||||
|
||||
@@ -189,9 +190,15 @@ export class DifficultyComponent implements OnInit {
|
||||
return shapes;
|
||||
}
|
||||
|
||||
@HostListener('pointerdown', ['$event'])
|
||||
onPointerDown(event) {
|
||||
this.onPointerMove(event);
|
||||
}
|
||||
|
||||
@HostListener('pointermove', ['$event'])
|
||||
onPointerMove(event) {
|
||||
this.tooltipPosition = { x: event.clientX, y: event.clientY };
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onHover(event, rect): void {
|
||||
|
||||
@@ -74,14 +74,14 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
|
||||
this.labelInterval = this.numSamples / this.numLabels;
|
||||
while (nextSample <= maxBlockVSize) {
|
||||
if (txIndex >= txs.length) {
|
||||
samples.push([(1 - (sampleIndex / this.numSamples)) * 100, 0]);
|
||||
samples.push([(1 - (sampleIndex / this.numSamples)) * 100, 0.000001]);
|
||||
nextSample += sampleInterval;
|
||||
sampleIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
while (txs[txIndex] && nextSample < cumVSize + txs[txIndex].vsize) {
|
||||
samples.push([(1 - (sampleIndex / this.numSamples)) * 100, txs[txIndex].rate]);
|
||||
samples.push([(1 - (sampleIndex / this.numSamples)) * 100, txs[txIndex].rate || 0.000001]);
|
||||
nextSample += sampleInterval;
|
||||
sampleIndex++;
|
||||
}
|
||||
@@ -118,7 +118,9 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
type: 'log',
|
||||
min: 1,
|
||||
max: this.data.reduce((min, val) => Math.max(min, val[1]), 1),
|
||||
// name: 'Effective Fee Rate s/vb',
|
||||
// nameLocation: 'middle',
|
||||
splitLine: {
|
||||
@@ -129,12 +131,16 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
|
||||
}
|
||||
},
|
||||
axisLabel: {
|
||||
show: true,
|
||||
formatter: (value: number): string => {
|
||||
const unitValue = this.weightMode ? value / 4 : value;
|
||||
const selectedPowerOfTen = selectPowerOfTen(unitValue);
|
||||
const newVal = Math.round(unitValue / selectedPowerOfTen.divider);
|
||||
return `${newVal}${selectedPowerOfTen.unit}`;
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
show: true,
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,9 @@ li.nav-item {
|
||||
|
||||
|
||||
.navbar-collapse {
|
||||
flex-basis: auto;
|
||||
@media (min-width: 564px) {
|
||||
flex-basis: auto;
|
||||
}
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`;
|
||||
@Input() allBlocks: boolean = false;
|
||||
|
||||
mempoolWidth: number = 0;
|
||||
@Output() widthChange: EventEmitter<number> = new EventEmitter();
|
||||
|
||||
specialBlocks = specialBlocks;
|
||||
@@ -49,6 +50,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
blockSubscription: Subscription;
|
||||
networkSubscription: Subscription;
|
||||
chainTipSubscription: Subscription;
|
||||
keySubscription: Subscription;
|
||||
isTabHiddenSubscription: Subscription;
|
||||
network = '';
|
||||
now = new Date().getTime();
|
||||
timeOffset = 0;
|
||||
@@ -115,8 +118,15 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.calculateTransactionPosition();
|
||||
});
|
||||
this.reduceMempoolBlocksToFitScreen(this.mempoolBlocks);
|
||||
this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
||||
this.loadingBlocks$ = this.stateService.isLoadingWebSocket$;
|
||||
this.isTabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
||||
this.loadingBlocks$ = combineLatest([
|
||||
this.stateService.isLoadingWebSocket$,
|
||||
this.stateService.isLoadingMempool$
|
||||
]).pipe(
|
||||
switchMap(([loadingBlocks, loadingMempool]) => {
|
||||
return of(loadingBlocks || loadingMempool);
|
||||
})
|
||||
);
|
||||
|
||||
this.mempoolBlocks$ = merge(
|
||||
of(true),
|
||||
@@ -155,7 +165,11 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
}),
|
||||
tap(() => {
|
||||
this.cd.markForCheck();
|
||||
this.widthChange.emit(this.containerOffset + this.mempoolBlocks.length * this.blockOffset);
|
||||
const width = this.containerOffset + this.mempoolBlocks.length * this.blockOffset;
|
||||
if (this.mempoolWidth !== width) {
|
||||
this.mempoolWidth = width;
|
||||
this.widthChange.emit(this.mempoolWidth);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -212,7 +226,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.networkSubscription = this.stateService.networkChanged$
|
||||
.subscribe((network) => this.network = network);
|
||||
|
||||
this.stateService.keyNavigation$.subscribe((event) => {
|
||||
this.keySubscription = this.stateService.keyNavigation$.subscribe((event) => {
|
||||
if (this.markIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -223,13 +237,12 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (this.mempoolBlocks[this.markIndex - 1]) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('mempool-block/'), this.markIndex - 1]);
|
||||
} else {
|
||||
this.stateService.blocks$
|
||||
.pipe(map((blocks) => blocks[0]))
|
||||
.subscribe((block) => {
|
||||
if (this.stateService.latestBlockHeight === block.height) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
|
||||
}
|
||||
});
|
||||
const blocks = this.stateService.blocksSubject$.getValue();
|
||||
for (const block of (blocks || [])) {
|
||||
if (this.stateService.latestBlockHeight === block.height) {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/block/'), block.id], { state: { data: { block } }});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (event.key === nextKey) {
|
||||
if (this.mempoolBlocks[this.markIndex + 1]) {
|
||||
@@ -253,6 +266,8 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||
this.networkSubscription.unsubscribe();
|
||||
this.timeLtrSubscription.unsubscribe();
|
||||
this.chainTipSubscription.unsubscribe();
|
||||
this.keySubscription.unsubscribe();
|
||||
this.isTabHiddenSubscription.unsubscribe();
|
||||
clearTimeout(this.resetTransitionTimeout);
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<app-blocks-list [attr.data-cy]="'latest-blocks'" [widget]=true></app-blocks-list>
|
||||
</div>
|
||||
@@ -65,7 +65,7 @@
|
||||
<a class="title-link" href="" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.adjustments">Adjustments</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<app-difficulty-adjustments-table [attr.data-cy]="'difficulty-adjustments-table'"></app-difficulty-adjustments-table>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { EventType, NavigationStart, Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-mining-dashboard',
|
||||
@@ -8,10 +10,12 @@ import { WebsocketService } from '../../services/websocket.service';
|
||||
styleUrls: ['./mining-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class MiningDashboardComponent implements OnInit {
|
||||
export class MiningDashboardComponent implements OnInit, AfterViewInit {
|
||||
constructor(
|
||||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
private stateService: StateService,
|
||||
private router: Router
|
||||
) {
|
||||
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Mining Dashboard`);
|
||||
}
|
||||
@@ -19,4 +23,15 @@ export class MiningDashboardComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
this.router.events.subscribe((e: NavigationStart) => {
|
||||
if (e.type === EventType.NavigationStart) {
|
||||
if (e.url.indexOf('graphs') === -1) { // The mining dashboard and the graph component are part of the same module so we can't use ngAfterViewInit in graphs.component.ts to blur the input
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,8 @@
|
||||
<td class="" *ngIf="this.miningWindowPreference === '24h'"><b>{{ miningStats.lastEstimatedHashrate}} {{
|
||||
miningStats.miningUnits.hashrateUnit }}</b></td>
|
||||
<td class=""><b>{{ miningStats.blockCount }}</b></td>
|
||||
<td *ngIf="auditAvailable"></td>
|
||||
<td *ngIf="auditAvailable"></td>
|
||||
<td class="d-none d-md-table-cell"><b>{{ miningStats.totalEmptyBlock }} ({{ miningStats.totalEmptyBlockRatio
|
||||
}}%)</b></td>
|
||||
</tr>
|
||||
|
||||
@@ -89,7 +89,7 @@ export class PoolPreviewComponent implements OnInit {
|
||||
|
||||
this.openGraphService.waitOver('pool-stats-' + this.slug);
|
||||
|
||||
const logoSrc = `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
||||
const logoSrc = `/resources/mining-pools/` + poolStats.pool.slug + '.svg';
|
||||
if (logoSrc === this.lastImgSrc) {
|
||||
this.openGraphService.waitOver('pool-img-' + this.slug);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export class PoolComponent implements OnInit {
|
||||
poolStats.pool.regexes = regexes.slice(0, -3);
|
||||
|
||||
return Object.assign({
|
||||
logo: `/resources/mining-pools/` + poolStats.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg'
|
||||
logo: `/resources/mining-pools/` + poolStats.pool.slug + '.svg'
|
||||
}, poolStats);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
<h4>TRUST YOUR OWN SELF-HOSTED MEMPOOL EXPLORER</h4>
|
||||
|
||||
<p>For maximum privacy, we recommend that you use your own self-hosted instance of The Mempool Open Source Project™ on your own hardware. You can easily install your own self-hosted instance of this website on a Raspberry Pi using a one-click installation method maintained by various Bitcoin fullnode distributions such as Umbrel, RaspiBlitz, MyNode, and RoninDojo. See our project's GitHub page for more details about self-hosting this website.</p>
|
||||
<p>For maximum privacy, we recommend that you use your own self-hosted instance of The Mempool Open Source Project® on your own hardware. You can easily install your own self-hosted instance of this website on a Raspberry Pi using a one-click installation method maintained by various Bitcoin fullnode distributions such as Umbrel, RaspiBlitz, MyNode, and RoninDojo. See our project's GitHub page for more details about self-hosting this website.</p>
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
|
||||
<div class="d-flex">
|
||||
<div class="search-box-container mr-2">
|
||||
<input autofocus (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
||||
<input #searchInput (focus)="focus$.next($any($event).target.value)" (click)="click$.next($any($event).target.value)" formControlName="searchText" type="text" class="form-control" i18n-placeholder="search-form.searchbar-placeholder" placeholder="Explore the full Bitcoin ecosystem">
|
||||
<app-search-results #searchResults [hidden]="dropdownHidden" [results]="typeAhead$ | async" (selectedResult)="selectedResult($event)"></app-search-results>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
|
||||
form {
|
||||
margin-top: 5px;
|
||||
@media (min-width: 576px) {
|
||||
@media (min-width: 564px) {
|
||||
margin-top: 0px;
|
||||
margin-left: 8px;
|
||||
margin-left: 5px;
|
||||
margin-right: -5px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
width: 100%;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, EventEmitter, Output, ViewChild, HostListener, ElementRef } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { EventType, NavigationStart, Router } from '@angular/router';
|
||||
import { AssetsService } from '../../services/assets.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Observable, of, Subject, zip, BehaviorSubject, combineLatest } from 'rxjs';
|
||||
@@ -34,7 +34,7 @@ export class SearchFormComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/;
|
||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64})$/;
|
||||
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
||||
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
|
||||
regexBlockheight = /^[0-9]{1,9}$/;
|
||||
@@ -47,6 +47,8 @@ export class SearchFormComponent implements OnInit {
|
||||
this.handleKeyDown($event);
|
||||
}
|
||||
|
||||
@ViewChild('searchInput') searchInput: ElementRef;
|
||||
|
||||
constructor(
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private router: Router,
|
||||
@@ -55,11 +57,26 @@ export class SearchFormComponent implements OnInit {
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private apiService: ApiService,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private elementRef: ElementRef,
|
||||
) { }
|
||||
private elementRef: ElementRef
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||
|
||||
this.router.events.subscribe((e: NavigationStart) => { // Reset search focus when changing page
|
||||
if (this.searchInput && e.type === EventType.NavigationStart) {
|
||||
this.searchInput.nativeElement.blur();
|
||||
}
|
||||
});
|
||||
|
||||
this.stateService.searchFocus$.subscribe(() => {
|
||||
if (!this.searchInput) { // Try again a bit later once the view is properly initialized
|
||||
setTimeout(() => this.searchInput.nativeElement.focus(), 100);
|
||||
} else if (this.searchInput) {
|
||||
this.searchInput.nativeElement.focus();
|
||||
}
|
||||
});
|
||||
|
||||
this.searchForm = this.formBuilder.group({
|
||||
searchText: ['', Validators.required],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input } from '@angular/core';
|
||||
import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild, Input, DoCheck } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { MarkBlockState, StateService } from '../../services/state.service';
|
||||
import { specialBlocks } from '../../app.constants';
|
||||
@@ -9,7 +9,7 @@ import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||
templateUrl: './start.component.html',
|
||||
styleUrls: ['./start.component.scss'],
|
||||
})
|
||||
export class StartComponent implements OnInit, OnDestroy {
|
||||
export class StartComponent implements OnInit, OnDestroy, DoCheck {
|
||||
@Input() showLoadingIndicator = false;
|
||||
|
||||
interval = 60;
|
||||
@@ -43,6 +43,7 @@ export class StartComponent implements OnInit, OnDestroy {
|
||||
pageIndex: number = 0;
|
||||
pages: any[] = [];
|
||||
pendingMark: number | null = null;
|
||||
pendingOffset: number | null = null;
|
||||
lastUpdate: number = 0;
|
||||
lastMouseX: number;
|
||||
velocity: number = 0;
|
||||
@@ -54,6 +55,14 @@ export class StartComponent implements OnInit, OnDestroy {
|
||||
this.isiOS = ['iPhone','iPod','iPad'].includes((navigator as any)?.userAgentData?.platform || navigator.platform);
|
||||
}
|
||||
|
||||
ngDoCheck(): void {
|
||||
if (this.pendingOffset != null) {
|
||||
const offset = this.pendingOffset;
|
||||
this.pendingOffset = null;
|
||||
this.addConvertedScrollOffset(offset);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
|
||||
this.blockCounterSubscription = this.stateService.blocks$.subscribe((blocks) => {
|
||||
@@ -429,6 +438,7 @@ export class StartComponent implements OnInit, OnDestroy {
|
||||
|
||||
addConvertedScrollOffset(offset: number): void {
|
||||
if (!this.blockchainContainer?.nativeElement) {
|
||||
this.pendingOffset = offset;
|
||||
return;
|
||||
}
|
||||
if (this.timeLtr) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
<div *ngIf="officialMempoolSpace">
|
||||
<h2>Trademark Policy and Guidelines</h2>
|
||||
<h5>The Mempool Open Source Project ™</h5>
|
||||
<h5>The Mempool Open Source Project ®</h5>
|
||||
<h6>Updated: July 19, 2021</h6>
|
||||
<br>
|
||||
|
||||
@@ -304,7 +304,7 @@
|
||||
|
||||
<p>Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:</p>
|
||||
|
||||
<p>“The Mempool Space K.K.™, The Mempool Open Source Project™, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein.”</p>
|
||||
<p>“The Mempool Space K.K.™, The Mempool Open Source Project®, mempool.space™, the mempool logo®, the mempool.space logos™, the mempool square logo®, and the mempool blocks logo™ are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein.”</p>
|
||||
|
||||
<li>What to Do When You See Abuse</li>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx.vin.slice(0, getVinLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': (assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded) || inputIndex === vindex,
|
||||
'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
|
||||
'highlight': this.address !== '' && (vin.prevout?.scriptpubkey_address === this.address || (vin.prevout?.scriptpubkey_type === 'p2pk' && vin.prevout?.scriptpubkey.slice(2, -2) === this.address))
|
||||
}">
|
||||
<td class="arrow-td">
|
||||
<ng-template [ngIf]="vin.prevout === null && !vin.is_pegin" [ngIfElse]="hasPrevout">
|
||||
@@ -56,7 +56,9 @@
|
||||
<span i18n="transactions-list.peg-in">Peg-in</span>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk'">
|
||||
<span>P2PK</span>
|
||||
<span>P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey.slice(2, -2)]" title="{{ vin.prevout.scriptpubkey.slice(2, -2) }}">
|
||||
<app-truncate [text]="vin.prevout.scriptpubkey.slice(2, -2)" [lastChars]="8"></app-truncate>
|
||||
</a></span>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
<ng-template [ngIf]="!vin.prevout" [ngIfElse]="defaultAddress">
|
||||
@@ -182,12 +184,19 @@
|
||||
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx.vout.slice(0, getVoutLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
|
||||
'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
|
||||
'highlight': this.address !== '' && (vout.scriptpubkey_address === this.address || (vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey.slice(2, -2) === this.address))
|
||||
}">
|
||||
<td class="address-cell">
|
||||
<a class="address" *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
|
||||
<a class="address" *ngIf="vout.scriptpubkey_address; else pubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
|
||||
<app-truncate [text]="vout.scriptpubkey_address" [lastChars]="8"></app-truncate>
|
||||
</a>
|
||||
<ng-template #pubkey_type>
|
||||
<ng-container *ngIf="vout.scriptpubkey_type === 'p2pk'; else scriptpubkey_type">
|
||||
P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey.slice(2, -2)]" title="{{ vout.scriptpubkey.slice(2, -2) }}">
|
||||
<app-truncate [text]="vout.scriptpubkey.slice(2, -2)" [lastChars]="8"></app-truncate>
|
||||
</a>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
<div>
|
||||
<app-address-labels [vout]="vout" [channel]="tx._channels && tx._channels.outputs[vindex] ? tx._channels.outputs[vindex] : null"></app-address-labels>
|
||||
</div>
|
||||
|
||||
@@ -149,6 +149,15 @@ h2 {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.p2pk-address {
|
||||
display: inline-block;
|
||||
margin-left: 1em;
|
||||
max-width: 100px;
|
||||
@media (min-width: 576px) {
|
||||
max-width: 200px
|
||||
}
|
||||
}
|
||||
|
||||
.grey-info-text {
|
||||
color:#6c757d;
|
||||
font-style: italic;
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<a class="title-link" href="" [routerLink]="['/rbf' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.latest-rbf-replacements">Latest replacements</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<table class="table lastest-replacements-table">
|
||||
<thead>
|
||||
@@ -112,7 +112,7 @@
|
||||
<a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="dashboard.latest-blocks">Latest blocks</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<table class="table lastest-blocks-table">
|
||||
<thead>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { combineLatest, merge, Observable, of, Subscription } from 'rxjs';
|
||||
import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
|
||||
import { catchError, filter, map, scan, share, switchMap, tap } from 'rxjs/operators';
|
||||
import { BlockExtended, OptimizedMempoolStats } from '../interfaces/node-api.interface';
|
||||
import { MempoolInfo, TransactionStripped, ReplacementInfo } from '../interfaces/websocket.interface';
|
||||
import { ApiService } from '../services/api.service';
|
||||
import { StateService } from '../services/state.service';
|
||||
@@ -31,7 +31,7 @@ interface MempoolStatsData {
|
||||
styleUrls: ['./dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DashboardComponent implements OnInit, OnDestroy {
|
||||
export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
featuredAssets$: Observable<any>;
|
||||
network$: Observable<string>;
|
||||
mempoolBlocksData$: Observable<MempoolBlocksData>;
|
||||
@@ -57,6 +57,10 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
private seoService: SeoService
|
||||
) { }
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.currencySubscription.unsubscribe();
|
||||
this.websocketService.stopTrackRbfSummary();
|
||||
@@ -155,7 +159,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
for (const block of blocks) {
|
||||
// @ts-ignore: Need to add an extra field for the template
|
||||
block.extras.pool.logo = `/resources/mining-pools/` +
|
||||
block.extras.pool.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg';
|
||||
block.extras.pool.slug + '.svg';
|
||||
}
|
||||
}
|
||||
return of(blocks.slice(0, 6));
|
||||
@@ -167,7 +171,11 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
this.mempoolStats$ = this.stateService.connectionState$
|
||||
.pipe(
|
||||
filter((state) => state === 2),
|
||||
switchMap(() => this.apiService.list2HStatistics$()),
|
||||
switchMap(() => this.apiService.list2HStatistics$().pipe(
|
||||
catchError((e) => {
|
||||
return of(null);
|
||||
})
|
||||
)),
|
||||
switchMap((mempoolStats) => {
|
||||
return merge(
|
||||
this.stateService.live2Chart$
|
||||
@@ -182,10 +190,14 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}),
|
||||
map((mempoolStats) => {
|
||||
return {
|
||||
mempool: mempoolStats,
|
||||
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
|
||||
};
|
||||
if (mempoolStats) {
|
||||
return {
|
||||
mempool: mempoolStats,
|
||||
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
<div class="doc-content">
|
||||
|
||||
<div id="disclaimer">
|
||||
<table *ngIf="!mobileViewport"><tr><td><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images></td><td><p i18n="faq.big-disclaimer"><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, confirming your transaction quicker, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></td></tr></table>
|
||||
<div *ngIf="mobileViewport"><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images><p i18n="faq.big-disclaimer"><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, confirming your transaction quicker, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></div>
|
||||
<table *ngIf="!mobileViewport"><tr><td><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images></td><td><p i18n="faq.big-disclaimer"><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, wallet issues, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></td></tr></table>
|
||||
<div *ngIf="mobileViewport"><app-svg-images name="warning" class="disclaimer-warning" viewBox="0 0 304 304" fill="#ffc107" width="50" height="50"></app-svg-images><p i18n="faq.big-disclaimer"><b>mempool.space merely provides data about the Bitcoin network.</b> It cannot help you with retrieving funds, wallet issues, etc.</p><p>For any such requests, you need to get in touch with the entity that helped make the transaction (wallet software, exchange company, etc).</p></div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -129,6 +129,22 @@ export interface Address {
|
||||
address: string;
|
||||
chain_stats: ChainStats;
|
||||
mempool_stats: MempoolStats;
|
||||
is_pubkey?: boolean;
|
||||
}
|
||||
|
||||
export interface ScriptHash {
|
||||
electrum?: boolean;
|
||||
scripthash: string;
|
||||
chain_stats: ChainStats;
|
||||
mempool_stats: MempoolStats;
|
||||
}
|
||||
|
||||
export interface AddressOrScriptHash {
|
||||
electrum?: boolean;
|
||||
address?: string;
|
||||
scripthash?: string;
|
||||
chain_stats: ChainStats;
|
||||
mempool_stats: MempoolStats;
|
||||
}
|
||||
|
||||
export interface ChainStats {
|
||||
|
||||
@@ -110,6 +110,7 @@ export interface PoolInfo {
|
||||
regexes: string; // JSON array
|
||||
addresses: string; // JSON array
|
||||
emptyBlocks: number;
|
||||
slug: string;
|
||||
}
|
||||
export interface PoolStat {
|
||||
pool: PoolInfo;
|
||||
@@ -174,7 +175,7 @@ export interface TransactionStripped {
|
||||
vsize: number;
|
||||
value: number;
|
||||
rate?: number; // effective fee rate
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
|
||||
context?: 'projected' | 'actual';
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export interface TransactionStripped {
|
||||
vsize: number;
|
||||
value: number;
|
||||
rate?: number; // effective fee rate
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'fullrbf';
|
||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
|
||||
context?: 'projected' | 'actual';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,43 @@
|
||||
<div class="box">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td i18n="lightning.starting-balance|Channel starting balance">Starting balance</td>
|
||||
<td *ngIf="showStartingBalance && minStartingBalance === maxStartingBalance"><app-sats [satoshis]="minStartingBalance"></app-sats></td>
|
||||
<td *ngIf="showStartingBalance && minStartingBalance !== maxStartingBalance">{{ minStartingBalance | number : '1.0-0' }} - {{ maxStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></td>
|
||||
<td *ngIf="!showStartingBalance">?</td>
|
||||
</tr>
|
||||
<tr *ngIf="channel.status === 2">
|
||||
<td i18n="lightning.closing-balance|Channel closing balance">Closing balance</td>
|
||||
<td *ngIf="showClosingBalance && minClosingBalance === maxClosingBalance"><app-sats [satoshis]="minClosingBalance"></app-sats></td>
|
||||
<td *ngIf="showClosingBalance && minClosingBalance !== maxClosingBalance">{{ minClosingBalance | number : '1.0-0' }} - {{ maxClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></td>
|
||||
<td *ngIf="!showClosingBalance">?</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="starting-balance" *ngIf="showStartingBalance">
|
||||
<h5 i18n="lightning.starting-balance|Channel starting balance">Starting balance</h5>
|
||||
<div class="nodes">
|
||||
<h5 class="alias">{{ left.alias }}</h5>
|
||||
<h5 class="alias">{{ right.alias }}</h5>
|
||||
</div>
|
||||
<div class="balances">
|
||||
<div class="balance left">
|
||||
<span class="value" *ngIf="minStartingBalance !== maxStartingBalance">{{ minStartingBalance | number : '1.0-0' }} - {{ maxStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
<span class="value" *ngIf="minStartingBalance === maxStartingBalance">{{ minStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
</div>
|
||||
<div class="balance right">
|
||||
<span class="value" *ngIf="minStartingBalance !== maxStartingBalance">{{ channel.capacity - maxStartingBalance | number : '1.0-0' }} - {{ channel.capacity - minStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
<span class="value" *ngIf="minStartingBalance === maxStartingBalance">{{ channel.capacity - maxStartingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="balance-bar">
|
||||
<div class="bar left" [class.hide-value]="hideStartingLeft" [style]="startingBalanceStyle.left"></div>
|
||||
<div class="bar center" [style]="startingBalanceStyle.center"></div>
|
||||
<div class="bar right" [class.hide-value]="hideStartingRight" [style]="startingBalanceStyle.right"></div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="closing-balance" *ngIf="showClosingBalance">
|
||||
<h5 i18n="lightning.closing-balance|Channel closing balance">Closing balance</h5>
|
||||
<div class="balances">
|
||||
<div class="balance left">
|
||||
<span class="value" *ngIf="minClosingBalance !== maxClosingBalance">{{ minClosingBalance | number : '1.0-0' }} - {{ maxClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
<span class="value" *ngIf="minClosingBalance === maxClosingBalance">{{ minClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
</div>
|
||||
<div class="balance right">
|
||||
<span class="value" *ngIf="minClosingBalance !== maxClosingBalance">{{ channel.capacity - maxClosingBalance | number : '1.0-0' }} - {{ channel.capacity - minClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
<span class="value" *ngIf="minClosingBalance === maxClosingBalance">{{ channel.capacity - maxClosingBalance | number : '1.0-0' }}<app-sats [valueOverride]=" "></app-sats></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="balance-bar">
|
||||
<div class="bar left" [class.hide-value]="hideClosingLeft" [style]="closingBalanceStyle.left"></div>
|
||||
<div class="bar center" [style]="closingBalanceStyle.center"></div>
|
||||
<div class="bar right" [class.hide-value]="hideClosingRight" [style]="closingBalanceStyle.right"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -6,4 +6,98 @@
|
||||
.box {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.starting-balance, .closing-balance {
|
||||
width: 100%;
|
||||
|
||||
h5 {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.nodes {
|
||||
display: none;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.balances {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.balance {
|
||||
&.left {
|
||||
text-align: start;
|
||||
}
|
||||
&.right {
|
||||
text-align: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.balance-bar {
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
position: relative;
|
||||
|
||||
.bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.left {
|
||||
background: #105fb0;
|
||||
}
|
||||
&.center {
|
||||
background: repeating-linear-gradient(
|
||||
60deg,
|
||||
#105fb0 0,
|
||||
#105fb0 12px,
|
||||
#1a9436 12px,
|
||||
#1a9436 24px
|
||||
);
|
||||
}
|
||||
&.right {
|
||||
background: #1a9436;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.hide-value {
|
||||
.value {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
height: 1em;
|
||||
|
||||
.bar.center {
|
||||
background: repeating-linear-gradient(
|
||||
60deg,
|
||||
#105fb0 0,
|
||||
#105fb0 8px,
|
||||
#1a9436 8px,
|
||||
#1a9436 16px
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges } f
|
||||
})
|
||||
export class ChannelCloseBoxComponent implements OnChanges {
|
||||
@Input() channel: any;
|
||||
@Input() local: any;
|
||||
@Input() remote: any;
|
||||
@Input() left: any;
|
||||
@Input() right: any;
|
||||
|
||||
showStartingBalance: boolean = false;
|
||||
showClosingBalance: boolean = false;
|
||||
@@ -18,29 +18,55 @@ export class ChannelCloseBoxComponent implements OnChanges {
|
||||
minClosingBalance: number;
|
||||
maxClosingBalance: number;
|
||||
|
||||
startingBalanceStyle: {
|
||||
left: string,
|
||||
center: string,
|
||||
right: string,
|
||||
} = {
|
||||
left: '',
|
||||
center: '',
|
||||
right: '',
|
||||
};
|
||||
|
||||
closingBalanceStyle: {
|
||||
left: string,
|
||||
center: string,
|
||||
right: string,
|
||||
} = {
|
||||
left: '',
|
||||
center: '',
|
||||
right: '',
|
||||
};
|
||||
|
||||
hideStartingLeft: boolean = false;
|
||||
hideStartingRight: boolean = false;
|
||||
hideClosingLeft: boolean = false;
|
||||
hideClosingRight: boolean = false;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (this.channel && this.local && this.remote) {
|
||||
this.showStartingBalance = (this.local.funding_balance || this.remote.funding_balance) && this.channel.funding_ratio;
|
||||
this.showClosingBalance = this.local.closing_balance || this.remote.closing_balance;
|
||||
let closingCapacity;
|
||||
if (this.channel && this.left && this.right) {
|
||||
this.showStartingBalance = (this.left.funding_balance || this.right.funding_balance) && this.channel.funding_ratio;
|
||||
this.showClosingBalance = this.left.closing_balance || this.right.closing_balance;
|
||||
|
||||
if (this.channel.single_funded) {
|
||||
if (this.local.funding_balance) {
|
||||
if (this.left.funding_balance) {
|
||||
this.minStartingBalance = this.channel.capacity;
|
||||
this.maxStartingBalance = this.channel.capacity;
|
||||
} else if (this.remote.funding_balance) {
|
||||
} else if (this.right.funding_balance) {
|
||||
this.minStartingBalance = 0;
|
||||
this.maxStartingBalance = 0;
|
||||
}
|
||||
} else {
|
||||
this.minStartingBalance = clampRound(0, this.channel.capacity, this.local.funding_balance * this.channel.funding_ratio);
|
||||
this.maxStartingBalance = clampRound(0, this.channel.capacity, this.channel.capacity - (this.remote.funding_balance * this.channel.funding_ratio));
|
||||
this.minStartingBalance = clampRound(0, this.channel.capacity, this.left.funding_balance * this.channel.funding_ratio);
|
||||
this.maxStartingBalance = clampRound(0, this.channel.capacity, this.channel.capacity - (this.right.funding_balance * this.channel.funding_ratio));
|
||||
}
|
||||
|
||||
const closingCapacity = this.channel.capacity - this.channel.closing_fee;
|
||||
this.minClosingBalance = clampRound(0, closingCapacity, this.local.closing_balance);
|
||||
this.maxClosingBalance = clampRound(0, closingCapacity, closingCapacity - this.remote.closing_balance);
|
||||
closingCapacity = this.channel.capacity - this.channel.closing_fee;
|
||||
this.minClosingBalance = clampRound(0, closingCapacity, this.left.closing_balance);
|
||||
this.maxClosingBalance = clampRound(0, closingCapacity, closingCapacity - this.right.closing_balance);
|
||||
|
||||
// margin of error to account for 2 x 330 sat anchor outputs
|
||||
if (Math.abs(this.minClosingBalance - this.maxClosingBalance) <= 660) {
|
||||
@@ -50,6 +76,26 @@ export class ChannelCloseBoxComponent implements OnChanges {
|
||||
this.showStartingBalance = false;
|
||||
this.showClosingBalance = false;
|
||||
}
|
||||
|
||||
const startingMinPc = (this.minStartingBalance / this.channel.capacity) * 100;
|
||||
const startingMaxPc = (this.maxStartingBalance / this.channel.capacity) * 100;
|
||||
this.startingBalanceStyle = {
|
||||
left: `left: 0%; right: ${100 - startingMinPc}%;`,
|
||||
center: `left: ${startingMinPc}%; right: ${100 -startingMaxPc}%;`,
|
||||
right: `left: ${startingMaxPc}%; right: 0%;`,
|
||||
};
|
||||
this.hideStartingLeft = startingMinPc < 15;
|
||||
this.hideStartingRight = startingMaxPc > 85;
|
||||
|
||||
const closingMinPc = (this.minClosingBalance / closingCapacity) * 100;
|
||||
const closingMaxPc = (this.maxClosingBalance / closingCapacity) * 100;
|
||||
this.closingBalanceStyle = {
|
||||
left: `left: 0%; right: ${100 - closingMinPc}%;`,
|
||||
center: `left: ${closingMinPc}%; right: ${100 - closingMaxPc}%;`,
|
||||
right: `left: ${closingMaxPc}%; right: 0%;`,
|
||||
};
|
||||
this.hideClosingLeft = closingMinPc < 15;
|
||||
this.hideClosingRight = closingMaxPc > 85;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,14 +75,14 @@
|
||||
<div class="row row-cols-1 row-cols-md-2" *ngIf="!error">
|
||||
<div class="col">
|
||||
<app-channel-box [channel]="channel.node_left"></app-channel-box>
|
||||
<app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [local]="channel.node_left" [remote]="channel.node_right"></app-channel-close-box>
|
||||
</div>
|
||||
<div class="col">
|
||||
<app-channel-box [channel]="channel.node_right"></app-channel-box>
|
||||
<app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [local]="channel.node_right" [remote]="channel.node_left"></app-channel-close-box>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-channel-close-box *ngIf="showCloseBoxes(channel)" [channel]="channel" [left]="channel.node_left" [right]="channel.node_right"></app-channel-close-box>
|
||||
|
||||
<br>
|
||||
|
||||
<ng-container *ngIf="transactions$ | async as transactions">
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/liquidity' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity Ranking</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-capacity>
|
||||
</div>
|
||||
@@ -75,7 +75,7 @@
|
||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/rankings/connectivity' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity Ranking</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-channels>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { share } from 'rxjs/operators';
|
||||
import { INodesRanking } from '../../interfaces/node-api.interface';
|
||||
@@ -12,7 +12,7 @@ import { LightningApiService } from '../lightning-api.service';
|
||||
styleUrls: ['./lightning-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LightningDashboardComponent implements OnInit {
|
||||
export class LightningDashboardComponent implements OnInit, AfterViewInit {
|
||||
statistics$: Observable<any>;
|
||||
nodesRanking$: Observable<INodesRanking>;
|
||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
@@ -30,4 +30,7 @@ export class LightningDashboardComponent implements OnInit {
|
||||
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h5 class="card-title d-inline" i18n="lightning.liquidity-ranking">Liquidity Ranking</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
|
||||
style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-capacity>
|
||||
</div>
|
||||
@@ -22,7 +22,7 @@
|
||||
<h5 class="card-title d-inline" i18n="lightning.connectivity-ranking">Connectivity Ranking</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
|
||||
style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-channels>
|
||||
</div>
|
||||
@@ -36,7 +36,7 @@
|
||||
<h5 class="card-title d-inline" i18n="lightning.top-channels-age">Oldest nodes</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
|
||||
style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<app-oldest-nodes [widget]="true"></app-oldest-nodes>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 250px);
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface';
|
||||
import { Observable, from, of, switchMap } from 'rxjs';
|
||||
import { Transaction, Address, Outspend, Recent, Asset, ScriptHash } from '../interfaces/electrs.interface';
|
||||
import { StateService } from './state.service';
|
||||
import { BlockExtended } from '../interfaces/node-api.interface';
|
||||
import { calcScriptHash$ } from '../bitcoin.utils';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -65,6 +66,25 @@ export class ElectrsApiService {
|
||||
return this.httpClient.get<Address>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address);
|
||||
}
|
||||
|
||||
getPubKeyAddress$(pubkey: string): Observable<Address> {
|
||||
const scriptpubkey = (pubkey.length === 130 ? '41' : '21') + pubkey + 'ac';
|
||||
return this.getScriptHash$(scriptpubkey).pipe(
|
||||
switchMap((scripthash: ScriptHash) => {
|
||||
return of({
|
||||
...scripthash,
|
||||
address: pubkey,
|
||||
is_pubkey: true,
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getScriptHash$(script: string): Observable<ScriptHash> {
|
||||
return from(calcScriptHash$(script)).pipe(
|
||||
switchMap(scriptHash => this.httpClient.get<ScriptHash>(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash))
|
||||
);
|
||||
}
|
||||
|
||||
getAddressTransactions$(address: string, txid?: string): Observable<Transaction[]> {
|
||||
let params = new HttpParams();
|
||||
if (txid) {
|
||||
@@ -73,6 +93,16 @@ export class ElectrsApiService {
|
||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
|
||||
}
|
||||
|
||||
getScriptHashTransactions$(script: string, txid?: string): Observable<Transaction[]> {
|
||||
let params = new HttpParams();
|
||||
if (txid) {
|
||||
params = params.append('after_txid', txid);
|
||||
}
|
||||
return from(calcScriptHash$(script)).pipe(
|
||||
switchMap(scriptHash => this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash + '/txs', { params })),
|
||||
);
|
||||
}
|
||||
|
||||
getAsset$(assetId: string): Observable<Asset> {
|
||||
return this.httpClient.get<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export class MiningService {
|
||||
share: parseFloat((poolStat.blockCount / stats.blockCount * 100).toFixed(2)),
|
||||
lastEstimatedHashrate: (poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider).toFixed(2),
|
||||
emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2),
|
||||
logo: `/resources/mining-pools/` + poolStat.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg',
|
||||
logo: `/resources/mining-pools/` + poolStat.slug + '.svg',
|
||||
...poolStat
|
||||
};
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Router, NavigationStart } from '@angular/router';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { filter, map, scan, shareReplay } from 'rxjs/operators';
|
||||
import { StorageService } from './storage.service';
|
||||
import { hasTouchScreen } from '../shared/pipes/bytes-pipe/utils';
|
||||
|
||||
export interface MarkBlockState {
|
||||
blockHeight?: number;
|
||||
@@ -113,6 +114,7 @@ export class StateService {
|
||||
mempoolTxPosition$ = new Subject<{ txid: string, position: MempoolPosition}>();
|
||||
blockTransactions$ = new Subject<Transaction>();
|
||||
isLoadingWebSocket$ = new ReplaySubject<boolean>(1);
|
||||
isLoadingMempool$ = new BehaviorSubject<boolean>(true);
|
||||
vbytesPerSecond$ = new ReplaySubject<number>(1);
|
||||
previousRetarget$ = new ReplaySubject<number>(1);
|
||||
backendInfo$ = new ReplaySubject<IBackendInfo>(1);
|
||||
@@ -138,6 +140,8 @@ export class StateService {
|
||||
fiatCurrency$: BehaviorSubject<string>;
|
||||
rateUnits$: BehaviorSubject<string>;
|
||||
|
||||
searchFocus$: Subject<boolean> = new Subject<boolean>();
|
||||
|
||||
constructor(
|
||||
@Inject(PLATFORM_ID) private platformId: any,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
@@ -355,4 +359,10 @@ export class StateService {
|
||||
this.blocks = this.blocks.slice(0, this.env.KEEP_BLOCKS_AMOUNT);
|
||||
this.blocksSubject$.next(this.blocks);
|
||||
}
|
||||
|
||||
focusSearchInputDesktop() {
|
||||
if (!hasTouchScreen()) {
|
||||
this.searchFocus$.next(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export class WebsocketService {
|
||||
this.stateService.connectionState$.next(2);
|
||||
}
|
||||
|
||||
if (this.stateService.connectionState$.value === 1) {
|
||||
if (this.stateService.connectionState$.value !== 2) {
|
||||
this.stateService.connectionState$.next(2);
|
||||
}
|
||||
|
||||
@@ -368,6 +368,11 @@ export class WebsocketService {
|
||||
|
||||
if (response.loadingIndicators) {
|
||||
this.stateService.loadingIndicators$.next(response.loadingIndicators);
|
||||
if (response.loadingIndicators.mempool != null && response.loadingIndicators.mempool < 100) {
|
||||
this.stateService.isLoadingMempool$.next(true);
|
||||
} else {
|
||||
this.stateService.isLoadingMempool$.next(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.mempoolInfo) {
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
<div class="container-fluid">
|
||||
<div class="row main">
|
||||
<div class="offset-lg-1 col-lg-4 col align-self-center branding">
|
||||
<h5><ng-container i18n="about.about-the-project">The Mempool Open Source Project</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></h5>
|
||||
<div class="main-logo">
|
||||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||
</div>
|
||||
<p><ng-container i18n="@@7deec1c1520f06170e1f8e8ddfbe4532312f638f">Explore the full Bitcoin ecosystem</ng-container><ng-template [ngIf]="locale.substr(0, 2) === 'en'"> ™</ng-template></p>
|
||||
<div class="selector">
|
||||
<app-language-selector></app-language-selector>
|
||||
@@ -17,17 +20,16 @@
|
||||
<a *ngIf="officialMempoolSpace" class="cta btn btn-purple sponsor" [routerLink]="['/signup' | relativeUrl]">Support the Project</a>
|
||||
<p *ngIf="officialMempoolSpace && env.BASE_MODULE === 'mempool'" class="cta-secondary"><a [routerLink]="['/signin' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Sign In</a></p>
|
||||
</ng-template>
|
||||
<p class="cta-secondary"><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p>
|
||||
<p *ngIf="officialMempoolSpace && env.LIGHTNING" class="cta-secondary"><a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]" i18n="footer.connect-to-our-nodes">Connect to our Nodes</a></p>
|
||||
<p><a [routerLink]="['/about' | relativeUrl]">About The Mempool Open Source Project™</a></p>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-10 offset-md-1 links outer">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<p class="category">Explore</p>
|
||||
<p><a [routerLink]="['/mining' | relativeUrl]">Mining Dashboard</a></p>
|
||||
<p><a [routerLink]="['/lightning' | relativeUrl]">Lightning Dashboard</a></p>
|
||||
<p><a *ngIf="env.LIGHTNING" [routerLink]="['/lightning' | relativeUrl]">Lightning Dashboard</a></p>
|
||||
<p><a [routerLink]="['/blocks' | relativeUrl]">Recent Blocks</a></p>
|
||||
<p><a [routerLink]="['/tx/push' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</a></p>
|
||||
<p *ngIf="officialMempoolSpace"><a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]" i18n="footer.connect-to-our-nodes">Connect to our Nodes</a></p>
|
||||
<p><a [routerLink]="['/docs/api' | relativeUrl]">API Documentation</a></p>
|
||||
</div>
|
||||
<div class="col-lg-6 links">
|
||||
@@ -38,25 +40,25 @@
|
||||
<p><a [routerLink]="['/docs/faq']" fragment="why-is-transaction-stuck-in-mempool">Why isn't my transaction confirming?</a></p>
|
||||
<p><a [routerLink]="['/docs/faq' | relativeUrl]">More FAQs ›</a></p>
|
||||
</div>
|
||||
<!--<div class="col-lg-4 links">
|
||||
<p class="category">Connect</p>
|
||||
<p><a href="https://github.com/mempool" target="_blank">GitHub</a></p>
|
||||
<p><a href="https://twitter.com/mempool" target="_blank">Twitter</a></p>
|
||||
<p><a href="nostr:npub18d4r6wanxkyrdfjdrjqzj2ukua5cas669ew2g5w7lf4a8te7awzqey6lt3" target="_blank">Nostr</a></p>
|
||||
<p><a href="https://youtube.com/@mempool" target="_blank">YouTube</a></p>
|
||||
<p><a href="https://bitcointv.com/c/mempool/videos" target="_blank">BitcoinTV</a></p>
|
||||
<p><a href="https://mempool.chat" target="_blank">Matrix</a></p>
|
||||
</div>-->
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-6 links">
|
||||
<p class="category">More Networks</p>
|
||||
<p *ngIf="currentNetwork !== '' && currentNetwork !== 'mainnet'"><a [href]="networkLink('mainnet')">Mainnet Explorer</a></p>
|
||||
<p *ngIf="currentNetwork !== 'testnet'"><a [href]="networkLink('testnet')">Testnet Explorer</a></p>
|
||||
<p *ngIf="currentNetwork !== 'signet'"><a [href]="networkLink('signet')">Signet Explorer</a></p>
|
||||
<p *ngIf="currentNetwork !== 'liquid' && currentNetwork !== 'liquidtestnet'"><a [href]="networkLink('liquid')">Liquid Explorer</a></p>
|
||||
<p *ngIf="currentNetwork !== 'bisq'"><a [href]="networkLink('bisq')">Bisq Explorer</a></p>
|
||||
<div class="col-lg-6 links" *ngIf="officialMempoolSpace || env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.LIQUID_TESTNET_ENABLED else toolBox" >
|
||||
<p class="category">Networks</p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== '') && (currentNetwork !== 'mainnet')"><a [href]="networkLink('mainnet')">Mainnet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'testnet') && env.TESTNET_ENABLED"><a [href]="networkLink('testnet')">Testnet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || (env.BASE_MODULE === 'mempool')) && (currentNetwork !== 'signet') && env.SIGNET_ENABLED"><a [href]="networkLink('signet')">Signet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquidtestnet')"><a [href]="networkLink('liquidtestnet')">Liquid Testnet Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace || env.LIQUID_ENABLED) && (currentNetwork !== 'liquid')"><a [href]="networkLink('liquid')">Liquid Explorer</a></p>
|
||||
<p *ngIf="(officialMempoolSpace && (currentNetwork !== 'bisq'))"><a [href]="networkLink('bisq')">Bisq Explorer</a></p>
|
||||
</div>
|
||||
<ng-template #toolBox>
|
||||
<div class="col-lg-6 links">
|
||||
<p class="category">Tools</p>
|
||||
<p><a [routerLink]="['/clock/mempool/0']">Clock (Mempool)</a></p>
|
||||
<p><a [routerLink]="['/clock/mined/0']">Clock (Mined)</a></p>
|
||||
<p><a [routerLink]="['/tools/calculator']">BTC/Fiat Converter</a></p>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="col-lg-6 links">
|
||||
<p class="category">Legal</p>
|
||||
<p><a [routerLink]="['/terms-of-service']" i18n="shared.terms-of-service|Terms of Service">Terms of Service</a></p>
|
||||
|
||||
@@ -21,6 +21,10 @@ footer .row.main .branding {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer .row.main .branding > p {
|
||||
margin-bottom: 45px;
|
||||
}
|
||||
|
||||
footer .row.main .branding .btn {
|
||||
display: inline-block;
|
||||
color: #fff !important;
|
||||
@@ -89,6 +93,11 @@ footer .row.version p a {
|
||||
color: #09a3ba;
|
||||
}
|
||||
|
||||
.main-logo {
|
||||
max-width: 220px;
|
||||
margin: 0 auto 20px auto;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
|
||||
footer .row.main .links.outer {
|
||||
|
||||
@@ -309,3 +309,28 @@ export function takeWhile(input: any[], predicate: CollectionPredicate) {
|
||||
return takeUntil(input, (item: any, index: number | undefined, collection: any[] | undefined) =>
|
||||
!predicate(item, index, collection));
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
|
||||
export function hasTouchScreen(): boolean {
|
||||
let hasTouchScreen = false;
|
||||
if ('maxTouchPoints' in navigator) {
|
||||
hasTouchScreen = navigator.maxTouchPoints > 0;
|
||||
} else if ('msMaxTouchPoints' in navigator) {
|
||||
// @ts-ignore
|
||||
hasTouchScreen = navigator.msMaxTouchPoints > 0;
|
||||
} else {
|
||||
const mQ = matchMedia?.('(pointer:coarse)');
|
||||
if (mQ?.media === '(pointer:coarse)') {
|
||||
hasTouchScreen = !!mQ.matches;
|
||||
} else if ('orientation' in window) {
|
||||
hasTouchScreen = true; // deprecated, but good fallback
|
||||
} else {
|
||||
// @ts-ignore - Only as a last resort, fall back to user agent sniffing
|
||||
const UA = navigator.userAgent;
|
||||
hasTouchScreen =
|
||||
/\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
|
||||
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA);
|
||||
}
|
||||
}
|
||||
return hasTouchScreen;
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
<script src="/resources/config.js"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem.">
|
||||
<meta name="description" content="The Mempool Open Source Project® - Explore the full Bitcoin ecosystem.">
|
||||
|
||||
<meta property="og:image" content="https://bisq.markets/resources/bisq/bisq-markets-preview.png" />
|
||||
<meta property="og:image:type" content="image/jpeg" />
|
||||
@@ -15,7 +15,7 @@
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:site" content="https://bisq.markets/">
|
||||
<meta property="twitter:creator" content="@bisq_network">
|
||||
<meta property="twitter:title" content="The Mempool Open Source Project™">
|
||||
<meta property="twitter:title" content="The Mempool Open Source Project®">
|
||||
<meta property="twitter:description" content="Explore the full Bitcoin ecosystem with mempool.space™" />
|
||||
<meta property="twitter:image:src" content="https://bisq.markets/resources/bisq/bisq-markets-preview.png" />
|
||||
<meta property="twitter:domain" content="bisq.markets">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<script src="/resources/config.js"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem.">
|
||||
<meta name="description" content="The Mempool Open Source Project® - Explore the full Bitcoin ecosystem.">
|
||||
<meta property="og:image" content="https://liquid.network/resources/liquid/liquid-network-preview.png" />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="1000" />
|
||||
@@ -15,7 +15,7 @@
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:site" content="@mempool">
|
||||
<meta property="twitter:creator" content="@mempool">
|
||||
<meta property="twitter:title" content="The Mempool Open Source Project™">
|
||||
<meta property="twitter:title" content="The Mempool Open Source Project®">
|
||||
<meta property="twitter:description" content="Explore the full Bitcoin ecosystem with mempool.space™" />
|
||||
<meta property="twitter:image:src" content="https://liquid.network/resources/liquid/liquid-network-preview.png" />
|
||||
<meta property="twitter:domain" content="liquid.network">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<script src="/resources/config.js"></script>
|
||||
<base href="/">
|
||||
|
||||
<meta name="description" content="The Mempool Open Source Project™ - Explore the full Bitcoin ecosystem." />
|
||||
<meta name="description" content="The Mempool Open Source Project® - Explore the full Bitcoin ecosystem." />
|
||||
<meta property="og:image" content="https://mempool.space/resources/mempool-space-preview.png" />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="1000" />
|
||||
@@ -15,7 +15,7 @@
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:site" content="@mempool">
|
||||
<meta property="twitter:creator" content="@mempool">
|
||||
<meta property="twitter:title" content="The Mempool Open Source Project™">
|
||||
<meta property="twitter:title" content="The Mempool Open Source Project®">
|
||||
<meta property="twitter:description" content="Explore the full Bitcoin ecosystem with mempool.space™" />
|
||||
<meta property="twitter:image:src" content="https://mempool.space/resources/mempool-space-preview.png" />
|
||||
<meta property="twitter:domain" content="mempool.space">
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 308 KiB After Width: | Height: | Size: 289 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 726 KiB After Width: | Height: | Size: 289 KiB |
Reference in New Issue
Block a user