Merge branch 'master' into nymkappa/fix-one-bug-add-two-more

This commit is contained in:
nymkappa 2023-12-04 10:47:49 +09:00
commit e2eeaebfc7
No known key found for this signature in database
GPG Key ID: E155910B16E8BD04
25 changed files with 1035 additions and 583 deletions

View File

@ -15,11 +15,12 @@ class FeeApi {
constructor() { } constructor() { }
defaultFee = Common.isLiquid() ? 0.1 : 1; defaultFee = Common.isLiquid() ? 0.1 : 1;
minimumIncrement = Common.isLiquid() ? 0.1 : 1;
public getRecommendedFee(): RecommendedFees { public getRecommendedFee(): RecommendedFees {
const pBlocks = projectedBlocks.getMempoolBlocks(); const pBlocks = projectedBlocks.getMempoolBlocks();
const mPool = mempool.getMempoolInfo(); const mPool = mempool.getMempoolInfo();
const minimumFee = Math.ceil(mPool.mempoolminfee * 100000); const minimumFee = this.roundUpToNearest(mPool.mempoolminfee * 100000, this.minimumIncrement);
const defaultMinFee = Math.max(minimumFee, this.defaultFee); const defaultMinFee = Math.max(minimumFee, this.defaultFee);
if (!pBlocks.length) { if (!pBlocks.length) {
@ -58,7 +59,11 @@ class FeeApi {
const multiplier = (pBlock.blockVSize - 500000) / 500000; const multiplier = (pBlock.blockVSize - 500000) / 500000;
return Math.max(Math.round(useFee * multiplier), this.defaultFee); return Math.max(Math.round(useFee * multiplier), this.defaultFee);
} }
return Math.ceil(useFee); return this.roundUpToNearest(useFee, this.minimumIncrement);
}
private roundUpToNearest(value: number, nearest: number): number {
return Math.ceil(value / nearest) * nearest;
} }
} }

View File

@ -18,7 +18,7 @@ class Mempool {
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {}; private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
private spendMap = new Map<string, MempoolTransactionExtended>(); private spendMap = new Map<string, MempoolTransactionExtended>();
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0, private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 }; maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[], private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined; deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[], private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],

View File

@ -15,6 +15,13 @@ import bitcoinApi from '../bitcoin/bitcoin-api-factory';
import { IEsploraApi } from '../bitcoin/esplora-api.interface'; import { IEsploraApi } from '../bitcoin/esplora-api.interface';
import database from '../../database'; import database from '../../database';
interface DifficultyBlock {
timestamp: number,
height: number,
bits: number,
difficulty: number,
}
class Mining { class Mining {
private blocksPriceIndexingRunning = false; private blocksPriceIndexingRunning = false;
public lastHashrateIndexingDate: number | null = null; public lastHashrateIndexingDate: number | null = null;
@ -421,6 +428,7 @@ class Mining {
indexedHeights[height] = true; indexedHeights[height] = true;
} }
// gets {time, height, difficulty, bits} of blocks in ascending order of height
const blocks: any = await BlocksRepository.$getBlocksDifficulty(); const blocks: any = await BlocksRepository.$getBlocksDifficulty();
const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0)); const genesisBlock: IEsploraApi.Block = await bitcoinApi.$getBlock(await bitcoinApi.$getBlockHash(0));
let currentDifficulty = genesisBlock.difficulty; let currentDifficulty = genesisBlock.difficulty;
@ -436,41 +444,45 @@ class Mining {
}); });
} }
const oldestConsecutiveBlock = await BlocksRepository.$getOldestConsecutiveBlock(); if (!blocks?.length) {
if (config.MEMPOOL.INDEXING_BLOCKS_AMOUNT !== -1) { // no blocks in database yet
currentBits = oldestConsecutiveBlock.bits; return;
currentDifficulty = oldestConsecutiveBlock.difficulty;
} }
const oldestConsecutiveBlock = this.getOldestConsecutiveBlock(blocks);
currentBits = oldestConsecutiveBlock.bits;
currentDifficulty = oldestConsecutiveBlock.difficulty;
let totalBlockChecked = 0; let totalBlockChecked = 0;
let timer = new Date().getTime() / 1000; let timer = new Date().getTime() / 1000;
for (const block of blocks) { for (const block of blocks) {
// skip until the first block after the oldest consecutive block
if (block.height <= oldestConsecutiveBlock.height) {
continue;
}
// difficulty has changed between two consecutive blocks!
if (block.bits !== currentBits) { if (block.bits !== currentBits) {
if (indexedHeights[block.height] === true) { // Already indexed // skip if already indexed
if (block.height >= oldestConsecutiveBlock.height) { if (indexedHeights[block.height] !== true) {
currentDifficulty = block.difficulty; let adjustment = block.difficulty / currentDifficulty;
currentBits = block.bits; adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise
}
continue; await DifficultyAdjustmentsRepository.$saveAdjustments({
time: block.time,
height: block.height,
difficulty: block.difficulty,
adjustment: adjustment,
});
totalIndexed++;
} }
// update the current difficulty
let adjustment = block.difficulty / currentDifficulty; currentDifficulty = block.difficulty;
adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise currentBits = block.bits;
}
await DifficultyAdjustmentsRepository.$saveAdjustments({
time: block.time,
height: block.height,
difficulty: block.difficulty,
adjustment: adjustment,
});
totalIndexed++;
if (block.height >= oldestConsecutiveBlock.height) {
currentDifficulty = block.difficulty;
currentBits = block.bits;
}
}
totalBlockChecked++; totalBlockChecked++;
const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer)); const elapsedSeconds = Math.max(1, Math.round((new Date().getTime() / 1000) - timer));
@ -633,6 +645,17 @@ class Mining {
default: return 86400 * scale; default: return 86400 * scale;
} }
} }
// Finds the oldest block in a consecutive chain back from the tip
// assumes `blocks` is sorted in ascending height order
private getOldestConsecutiveBlock(blocks: DifficultyBlock[]): DifficultyBlock {
for (let i = blocks.length - 1; i > 0; i--) {
if ((blocks[i].height - blocks[i - 1].height) > 1) {
return blocks[i];
}
}
return blocks[0];
}
} }
export default new Mining(); export default new Mining();

View File

@ -94,9 +94,13 @@ class WebsocketHandler {
throw new Error('WebSocket.Server is not set'); throw new Error('WebSocket.Server is not set');
} }
this.wss.on('connection', (client: WebSocket) => { this.wss.on('connection', (client: WebSocket, req) => {
this.numConnected++; this.numConnected++;
client.on('error', logger.info); client['remoteAddress'] = req.headers['x-forwarded-for'] || req.socket?.remoteAddress || 'unknown';
client.on('error', (e) => {
logger.info(`websocket client error from ${client['remoteAddress']}: ` + (e instanceof Error ? e.message : e));
client.close();
});
client.on('close', () => { client.on('close', () => {
this.numDisconnected++; this.numDisconnected++;
}); });
@ -282,7 +286,8 @@ class WebsocketHandler {
client.send(serializedResponse); client.send(serializedResponse);
} }
} catch (e) { } catch (e) {
logger.debug('Error parsing websocket message: ' + (e instanceof Error ? e.message : e)); logger.debug(`Error parsing websocket message from ${client['remoteAddress']}: ` + (e instanceof Error ? e.message : e));
client.close();
} }
}); });
}); });

View File

@ -541,7 +541,7 @@ class BlocksRepository {
*/ */
public async $getBlocksDifficulty(): Promise<object[]> { public async $getBlocksDifficulty(): Promise<object[]> {
try { try {
const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty, bits FROM blocks`); const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty, bits FROM blocks ORDER BY height ASC`);
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot get blocks difficulty list from the db. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot get blocks difficulty list from the db. Reason: ' + (e instanceof Error ? e.message : e));

File diff suppressed because it is too large Load Diff

View File

@ -74,9 +74,9 @@
"@angular/platform-server": "^16.2.2", "@angular/platform-server": "^16.2.2",
"@angular/router": "^16.2.2", "@angular/router": "^16.2.2",
"@fortawesome/angular-fontawesome": "~0.13.0", "@fortawesome/angular-fontawesome": "~0.13.0",
"@fortawesome/fontawesome-common-types": "~6.4.0", "@fortawesome/fontawesome-common-types": "~6.5.1",
"@fortawesome/fontawesome-svg-core": "~6.4.0", "@fortawesome/fontawesome-svg-core": "~6.5.1",
"@fortawesome/free-solid-svg-icons": "~6.4.0", "@fortawesome/free-solid-svg-icons": "~6.5.1",
"@mempool/mempool.js": "2.3.0", "@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^15.1.0", "@ng-bootstrap/ng-bootstrap": "^15.1.0",
"@types/qrcode": "~1.5.0", "@types/qrcode": "~1.5.0",
@ -90,7 +90,7 @@
"ngx-infinite-scroll": "^16.0.0", "ngx-infinite-scroll": "^16.0.0",
"qrcode": "1.5.1", "qrcode": "1.5.1",
"rxjs": "~7.8.1", "rxjs": "~7.8.1",
"tinyify": "^4.0.0", "tinyify": "^3.1.0",
"tlite": "^0.1.9", "tlite": "^0.1.9",
"tslib": "~2.6.0", "tslib": "~2.6.0",
"zone.js": "~0.13.1" "zone.js": "~0.13.1"

View File

@ -1,16 +1,16 @@
<div id="become-sponsor-container"> <div id="become-sponsor-container">
<div class="become-sponsor community"> <div class="become-sponsor community">
<p style="font-weight: 700; font-size: 18px;">If you're an individual...</p> <p style="font-weight: 700; font-size: 18px;">If you're an individual...</p>
<a href="https://mempool.space/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button">Become a Community Sponsor</a> <a href="https://mempool.space/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button" (click)="onSponsorClick($event)">Become a Community Sponsor</a>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Exclusive swag</p> <p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Exclusive swag</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Your avatar on the About page</p> <p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Your avatar on the About page</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> And more coming soon :)</p> <p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> And more coming soon :)</p>
</div> </div>
<div class="become-sponsor enterprise"> <div class="become-sponsor enterprise">
<p style="font-weight: 700; font-size: 18px;">If you're a business...</p> <p style="font-weight: 700; font-size: 18px;">If you're a business...</p>
<a href="https://mempool.space/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button">Become an Enterprise Sponsor</a> <a href="https://mempool.space/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button" (click)="onEnterpriseClick($event)">Become an Enterprise Sponsor</a>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Increased API limits</p> <p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Increased API limits</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Co-branded instance</p> <p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> Co-branded instance</p>
<p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> 99% service-level agreement</p> <p class="sponsor-feature"><fa-icon [icon]="['fas', 'check']"></fa-icon> 99% service-level agreement</p>
</div> </div>
</div> </div>

View File

@ -9,6 +9,7 @@ import { Router, ActivatedRoute } from '@angular/router';
import { map, share, tap } from 'rxjs/operators'; import { map, share, tap } from 'rxjs/operators';
import { ITranslators } from '../../interfaces/node-api.interface'; import { ITranslators } from '../../interfaces/node-api.interface';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { EnterpriseService } from '../../services/enterprise.service';
@Component({ @Component({
selector: 'app-about', selector: 'app-about',
@ -33,6 +34,7 @@ export class AboutComponent implements OnInit {
private websocketService: WebsocketService, private websocketService: WebsocketService,
private seoService: SeoService, private seoService: SeoService,
public stateService: StateService, public stateService: StateService,
private enterpriseService: EnterpriseService,
private apiService: ApiService, private apiService: ApiService,
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
@ -121,4 +123,14 @@ export class AboutComponent implements OnInit {
unmutePromoVideo(): void { unmutePromoVideo(): void {
this.promoVideo.nativeElement.muted = false; this.promoVideo.nativeElement.muted = false;
} }
onSponsorClick(e): boolean {
this.enterpriseService.goal(5);
return true;
}
onEnterpriseClick(e): boolean {
this.enterpriseService.goal(6);
return true;
}
} }

View File

@ -27,6 +27,11 @@
<ng-container *ngIf="estimate"> <ng-container *ngIf="estimate">
<div [class]="{estimateDisabled: error}"> <div [class]="{estimateDisabled: error}">
<div *ngIf="!estimate.hasAccess">
<div class="alert alert-mempool">You are currently on the wait list</div>
</div>
<h5>Your transaction</h5> <h5>Your transaction</h5>
<div class="row"> <div class="row">
<div class="col"> <div class="col">
@ -230,7 +235,7 @@
<div class="row mb-3" *ngIf="isLoggedIn()"> <div class="row mb-3" *ngIf="isLoggedIn()">
<div class="col"> <div class="col">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end" *ngIf="estimate.hasAccess">
<button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()">Accelerate</button> <button class="btn btn-sm btn-primary btn-success" style="width: 150px" (click)="accelerate()">Accelerate</button>
</div> </div>
</div> </div>

View File

@ -93,7 +93,7 @@ export class AcceleratePreviewComponent implements OnInit, OnDestroy, OnChanges
this.estimateSubscription.unsubscribe(); this.estimateSubscription.unsubscribe();
} }
if (this.estimate.userBalance <= 0) { if (this.estimate.hasAccess === true && this.estimate.userBalance <= 0) {
if (this.isLoggedIn()) { if (this.isLoggedIn()) {
this.error = `not_enough_balance`; this.error = `not_enough_balance`;
this.scrollToPreviewWithTimeout('mempoolError', 'center'); this.scrollToPreviewWithTimeout('mempoolError', 'center');

View File

@ -69,7 +69,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
this.addressString = this.addressString.toLowerCase(); this.addressString = this.addressString.toLowerCase();
} }
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`); this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`); this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'} ${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/) return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
? this.electrsApiService.getPubKeyAddress$(this.addressString) ? this.electrsApiService.getPubKeyAddress$(this.addressString)

View File

@ -219,13 +219,13 @@
<div class="box" *ngIf="!error && webGlEnabled && showAudit"> <div class="box" *ngIf="!error && webGlEnabled && showAudit">
<div class="nav nav-tabs" *ngIf="isMobile && showAudit"> <div class="nav nav-tabs" *ngIf="isMobile && showAudit">
<a class="nav-link" [class.active]="mode === 'projected'" <a class="nav-link" [class.active]="mode === 'projected'"
fragment="projected" (click)="changeMode('projected')"><ng-container i18n="block.expected">Expected</ng-container>&nbsp;&nbsp;<span class="badge badge-pill badge-warning" i18n="beta">beta</span></a> fragment="projected" (click)="changeMode('projected')"><ng-container i18n="block.expected">Expected</ng-container></a>
<a class="nav-link" [class.active]="mode === 'actual'" i18n="block.actual" <a class="nav-link" [class.active]="mode === 'actual'" i18n="block.actual"
fragment="actual" (click)="changeMode('actual')">Actual</a> fragment="actual" (click)="changeMode('actual')">Actual</a>
</div> </div>
<div class="row"> <div class="row">
<div class="col-sm"> <div class="col-sm">
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container> <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span></h3> <h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container></h3>
<div class="block-graph-wrapper"> <div class="block-graph-wrapper">
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86" <app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86"
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit" [blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"

View File

@ -249,10 +249,8 @@ export class HashrateChartComponent implements OnInit {
for (const tick of ticks) { for (const tick of ticks) {
if (tick.seriesIndex === 0) { // Hashrate if (tick.seriesIndex === 0) { // Hashrate
let hashrate = tick.data[1]; let hashrate = tick.data[1];
if (this.isMobile()) { hashratePowerOfTen = selectPowerOfTen(tick.data[1]);
hashratePowerOfTen = selectPowerOfTen(tick.data[1]); hashrate = Math.round(tick.data[1] / hashratePowerOfTen.divider);
hashrate = Math.round(tick.data[1] / hashratePowerOfTen.divider);
}
hashrateString = `${tick.marker} ${tick.seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s<br>`; hashrateString = `${tick.marker} ${tick.seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s<br>`;
} else if (tick.seriesIndex === 1) { // Difficulty } else if (tick.seriesIndex === 1) { // Difficulty
let difficultyPowerOfTen = hashratePowerOfTen; let difficultyPowerOfTen = hashratePowerOfTen;
@ -260,18 +258,14 @@ export class HashrateChartComponent implements OnInit {
if (difficulty === null) { if (difficulty === null) {
difficultyString = `${tick.marker} ${tick.seriesName}: No data<br>`; difficultyString = `${tick.marker} ${tick.seriesName}: No data<br>`;
} else { } else {
if (this.isMobile()) { difficultyPowerOfTen = selectPowerOfTen(tick.data[1]);
difficultyPowerOfTen = selectPowerOfTen(tick.data[1]); difficulty = tick.data[1] / difficultyPowerOfTen.divider;
difficulty = Math.round(tick.data[1] / difficultyPowerOfTen.divider);
}
difficultyString = `${tick.marker} ${tick.seriesName}: ${formatNumber(difficulty, this.locale, '1.2-2')} ${difficultyPowerOfTen.unit}<br>`; difficultyString = `${tick.marker} ${tick.seriesName}: ${formatNumber(difficulty, this.locale, '1.2-2')} ${difficultyPowerOfTen.unit}<br>`;
} }
} else if (tick.seriesIndex === 2) { // Hashrate MA } else if (tick.seriesIndex === 2) { // Hashrate MA
let hashrate = tick.data[1]; let hashrate = tick.data[1];
if (this.isMobile()) { hashratePowerOfTen = selectPowerOfTen(tick.data[1]);
hashratePowerOfTen = selectPowerOfTen(tick.data[1]); hashrate = Math.round(tick.data[1] / hashratePowerOfTen.divider);
hashrate = Math.round(tick.data[1] / hashratePowerOfTen.divider);
}
hashrateStringMA = `${tick.marker} ${tick.seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s`; hashrateStringMA = `${tick.marker} ${tick.seriesName}: ${formatNumber(hashrate, this.locale, '1.0-0')} ${hashratePowerOfTen.unit}H/s`;
} }
} }

View File

@ -88,7 +88,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
this.seoService.setTitle( this.seoService.setTitle(
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:` $localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
); );
this.seoService.setDescription($localize`:@@meta.description.bitcoin.transaction:Get real-time status, addresses, fees, script info, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} transaction with txid {txid}.`); this.seoService.setDescription($localize`:@@meta.description.bitcoin.transaction:Get real-time status, addresses, fees, script info, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} transaction with txid ${this.txId}.`);
this.resetTransaction(); this.resetTransaction();
return merge( return merge(
of(true), of(true),

View File

@ -83,9 +83,13 @@
<!-- Accelerator --> <!-- Accelerator -->
<ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary"> <ng-container *ngIf="!tx?.status?.confirmed && showAccelerationSummary">
<div class="title mt-3"> <br>
<div class="title float-left">
<h2>Accelerate</h2> <h2>Accelerate</h2>
</div> </div>
<button type="button" class="btn btn-outline-info accelerator-toggle btn-sm float-right" (click)="showAccelerationSummary = false" i18n="hide-accelerator">Hide accelerator</button>
<div class="clearfix"></div>
<div class="box"> <div class="box">
<app-accelerate-preview [tx]="tx" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview> <app-accelerate-preview [tx]="tx" [scrollEvent]="scrollIntoAccelPreview"></app-accelerate-preview>
</div> </div>
@ -519,7 +523,7 @@
<div class="effective-fee-container"> <div class="effective-fee-container">
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate> <app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
<ng-template [ngIf]="tx?.status?.confirmed"> <ng-template [ngIf]="tx?.status?.confirmed">
<app-tx-fee-rating class="ml-2 mr-2" *ngIf="tx.fee || tx.effectiveFeePerVsize" [tx]="tx"></app-tx-fee-rating> <app-tx-fee-rating class="ml-2 mr-2 effective-fee-rating" *ngIf="tx.fee || tx.effectiveFeePerVsize" [tx]="tx"></app-tx-fee-rating>
</ng-template> </ng-template>
</div> </div>
<button *ngIf="cpfpInfo.bestDescendant || cpfpInfo.descendants?.length || cpfpInfo.ancestors?.length" type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button> <button *ngIf="cpfpInfo.bestDescendant || cpfpInfo.descendants?.length || cpfpInfo.ancestors?.length" type="button" class="btn btn-outline-info btn-sm btn-small-height float-right" (click)="showCpfpDetails = !showCpfpDetails">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button>

View File

@ -152,6 +152,16 @@
@media (min-width: 768px){ @media (min-width: 768px){
display: inline-block; display: inline-block;
} }
@media (max-width: 425px){
display: flex;
flex-direction: column;
}
}
.effective-fee-rating {
@media (max-width: 767px){
margin-right: 0px !important;
}
} }
.title { .title {
@ -169,7 +179,7 @@
} }
} }
.details-button, .flow-toggle { .details-button, .flow-toggle, .accelerator-toggle {
margin-top: -5px; margin-top: -5px;
margin-left: 10px; margin-left: 10px;
@media (min-width: 768px){ @media (min-width: 768px){

View File

@ -2922,7 +2922,7 @@ export const restApiDocsData = [
fragment: "get-blocks-bulk", fragment: "get-blocks-bulk",
title: "GET Blocks (Bulk)", title: "GET Blocks (Bulk)",
description: { description: {
default: "<p>Returns details on the range of blocks between <code>:minHeight</code> and <code>:maxHeight</code>, inclusive, up to 10 blocks. If <code>:maxHeight</code> is not specified, it defaults to the current tip.</p><p>To return data for more than 10 blocks, consider becoming an <a href='/enterprise'>enterprise sponsor</a>.</p>" default: "<p>Returns details on the range of blocks between <code>:minHeight</code> and <code>:maxHeight</code>, inclusive, up to 10 blocks. If <code>:maxHeight</code> is not specified, it defaults to the current tip.</p><p>To return data for more than 10 blocks, consider becoming an <a href='https://mempool.space/enterprise'>enterprise sponsor</a>.</p>"
}, },
urlString: "/v1/blocks-bulk/:minHeight[/:maxHeight]", urlString: "/v1/blocks-bulk/:minHeight[/:maxHeight]",
showConditions: bitcoinNetworks, showConditions: bitcoinNetworks,

View File

@ -40,7 +40,7 @@
<div class="doc-content"> <div class="doc-content">
<p class="doc-welcome-note">Below is a reference for the {{ network.val === '' ? 'Bitcoin' : network.val.charAt(0).toUpperCase() + network.val.slice(1) }} <ng-container i18n="api-docs.title">REST API service</ng-container>.</p> <p class="doc-welcome-note">Below is a reference for the {{ network.val === '' ? 'Bitcoin' : network.val.charAt(0).toUpperCase() + network.val.slice(1) }} <ng-container i18n="api-docs.title">REST API service</ng-container>.</p>
<p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that we enforce rate limits. If you exceed these limits, you will get an HTTP 429 error. If you repeatedly exceed the limits, you may be banned from accessing the service altogether. Consider an <a href="/enterprise">enterprise sponsorship</a> if you need higher API limits.</p> <p class="doc-welcome-note api-note" *ngIf="officialMempoolInstance">Note that we enforce rate limits. If you exceed these limits, you will get an HTTP 429 error. If you repeatedly exceed the limits, you may be banned from accessing the service altogether. Consider an <a href="https://mempool.space/enterprise">enterprise sponsorship</a> if you need higher API limits.</p>
<div class="doc-item-container" *ngFor="let item of restDocs"> <div class="doc-item-container" *ngFor="let item of restDocs">
<h3 *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</h3> <h3 *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</h3>
@ -123,7 +123,7 @@
<p>{{electrsPort}}</p> <p>{{electrsPort}}</p>
<p class="subtitle">SSL</p> <p class="subtitle">SSL</p>
<p>Enabled</p> <p>Enabled</p>
<p class="note" *ngIf="network.val !== 'signet'">Electrum RPC interface for Bitcoin Signet is <a href="/signet/docs/api/electrs">publicly available</a>. Electrum RPC interface for all other networks is available to <a href='/enterprise'>sponsors</a> only—whitelisting is required.</p> <p class="note" *ngIf="network.val !== 'signet'">Electrum RPC interface for Bitcoin Signet is <a href="/signet/docs/api/electrs">publicly available</a>. Electrum RPC interface for all other networks is available to <a href='https://mempool.space/enterprise'>sponsors</a> only—whitelisting is required.</p>
</div> </div>
</div> </div>
</div> </div>
@ -288,7 +288,7 @@
</ng-template> </ng-template>
<ng-template type="host-my-own-instance-server"> <ng-template type="host-my-own-instance-server">
<p>You can manually install Mempool on your own server, but this requires advanced sysadmin skills since you will be manually configuring everything. You could also use our <a href="https://github.com/mempool/mempool/tree/master/docker" target="_blank">Docker images</a>.</p><p>In any case, we only provide support for manual deployments to <a href="/enterprise">enterprise sponsors</a>.</p> <p>You can manually install Mempool on your own server, but this requires advanced sysadmin skills since you will be manually configuring everything. You could also use our <a href="https://github.com/mempool/mempool/tree/master/docker" target="_blank">Docker images</a>.</p><p>In any case, we only provide support for manual deployments to <a href="https://mempool.space/enterprise">enterprise sponsors</a>.</p>
<p>For casual users, we strongly suggest installing Mempool using one of the <a href="https://github.com/mempool/mempool#one-click-installation" target="_blank">1-click install methods</a>.</p> <p>For casual users, we strongly suggest installing Mempool using one of the <a href="https://github.com/mempool/mempool#one-click-installation" target="_blank">1-click install methods</a>.</p>
</ng-template> </ng-template>

View File

@ -25,7 +25,7 @@ export class LightningDashboardComponent implements OnInit, AfterViewInit {
ngOnInit(): void { ngOnInit(): void {
this.seoService.setTitle($localize`:@@142e923d3b04186ac6ba23387265d22a2fa404e0:Lightning Explorer`); this.seoService.setTitle($localize`:@@142e923d3b04186ac6ba23387265d22a2fa404e0:Lightning Explorer`);
this.seoService.setDescription($localize`:@@meta.description.lightning.dashboard:Get stats on the Lightning network (aggregate capacity, connectivity, etc) and Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc).`); this.seoService.setDescription($localize`:@@meta.description.lightning.dashboard:Get stats on the Lightning network (aggregate capacity, connectivity, etc), Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc).`);
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share()); this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share()); this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());

View File

@ -20,7 +20,7 @@ export class NodesRankingsDashboard implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.seoService.setTitle($localize`Top lightning nodes`); this.seoService.setTitle($localize`Top lightning nodes`);
this.seoService.setDescription($localize`:@@meta.description.lightning.rankings-dashboard:See top the Lightning network nodes ranked by liquidity, connectivity, and age.`); this.seoService.setDescription($localize`:@@meta.description.lightning.rankings-dashboard:See the top Lightning network nodes ranked by liquidity, connectivity, and age.`);
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share()); this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
} }
} }

View File

@ -3,6 +3,7 @@ import { Inject, Injectable } from '@angular/core';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
import { SeoService } from './seo.service'; import { SeoService } from './seo.service';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { ActivatedRoute } from '@angular/router';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -11,12 +12,15 @@ export class EnterpriseService {
exclusiveHostName = '.mempool.space'; exclusiveHostName = '.mempool.space';
subdomain: string | null = null; subdomain: string | null = null;
info: object = {}; info: object = {};
statsUrl: string;
siteId: number;
constructor( constructor(
@Inject(DOCUMENT) private document: Document, @Inject(DOCUMENT) private document: Document,
private apiService: ApiService, private apiService: ApiService,
private seoService: SeoService, private seoService: SeoService,
private stateService: StateService, private stateService: StateService,
private activatedRoute: ActivatedRoute,
) { ) {
const subdomain = this.document.location.hostname.indexOf(this.exclusiveHostName) > -1 const subdomain = this.document.location.hostname.indexOf(this.exclusiveHostName) > -1
&& this.document.location.hostname.split(this.exclusiveHostName)[0] || false; && this.document.location.hostname.split(this.exclusiveHostName)[0] || false;
@ -56,7 +60,7 @@ export class EnterpriseService {
insertMatomo(siteId?: number): void { insertMatomo(siteId?: number): void {
let statsUrl = '//stats.mempool.space/'; let statsUrl = '//stats.mempool.space/';
if (!siteId) { if (!siteId) {
switch (this.document.location.hostname) { switch (this.document.location.hostname) {
case 'mempool.space': case 'mempool.space':
@ -88,16 +92,63 @@ export class EnterpriseService {
} }
} }
this.statsUrl = statsUrl;
this.siteId = siteId;
// @ts-ignore // @ts-ignore
const _paq = window._paq = window._paq || []; if (window._paq && window['Matomo']) {
_paq.push(['disableCookies']); window['Matomo'].addTracker(statsUrl+'m.php', siteId.toString());
_paq.push(['trackPageView']); const matomo = this.getMatomo();
_paq.push(['enableLinkTracking']); matomo.setDocumentTitle(this.seoService.getTitle());
(function() { matomo.setCustomUrl(this.getCustomUrl());
_paq.push(['setTrackerUrl', statsUrl+'m.php']); matomo.disableCookies();
_paq.push(['setSiteId', siteId.toString()]); matomo.trackPageView();
const d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; matomo.enableLinkTracking();
g.type='text/javascript'; g.async=true; g.src=statsUrl+'m.js'; s.parentNode.insertBefore(g,s); } else {
})(); // @ts-ignore
const alreadyInitialized = !!window._paq;
// @ts-ignore
const _paq = window._paq = window._paq || [];
_paq.push(['setDocumentTitle', this.seoService.getTitle()]);
_paq.push(['setCustomUrl', this.getCustomUrl()]);
_paq.push(['disableCookies']);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
if (alreadyInitialized) {
_paq.push(['addTracker', statsUrl+'m.php', siteId.toString()]);
} else {
(function() {
_paq.push(['setTrackerUrl', statsUrl+'m.php']);
_paq.push(['setSiteId', siteId.toString()]);
const d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
// @ts-ignore
g.type='text/javascript'; g.async=true; g.src=statsUrl+'m.js'; s.parentNode.insertBefore(g,s);
})();
}
}
}
private getMatomo() {
if (this.siteId != null) {
return window['Matomo']?.getTracker(this.statsUrl+'m.php', this.siteId);
}
}
goal(id: number) {
// @ts-ignore
this.getMatomo()?.trackGoal(id);
}
private getCustomUrl(): string {
let url = window.location.origin + '/';
let route = this.activatedRoute;
while (route) {
const segment = route?.routeConfig?.path;
if (segment && segment.length) {
url += segment + '/';
}
route = route.firstChild;
}
return url;
} }
} }

View File

@ -76,17 +76,22 @@ export class WebsocketService {
this.stateService.resetChainTip(); this.stateService.resetChainTip();
this.websocketSubject.complete(); this.reconnectWebsocket();
this.subscription.unsubscribe();
this.websocketSubject = webSocket<WebsocketResponse>(
this.webSocketUrl.replace('{network}', this.network ? '/' + this.network : '')
);
this.startSubscription();
}); });
} }
} }
reconnectWebsocket(retrying = false, hasInitData = false) {
console.log('reconnecting websocket');
this.websocketSubject.complete();
this.subscription.unsubscribe();
this.websocketSubject = webSocket<WebsocketResponse>(
this.webSocketUrl.replace('{network}', this.network ? '/' + this.network : '')
);
this.startSubscription(retrying, hasInitData);
}
startSubscription(retrying = false, hasInitData = false) { startSubscription(retrying = false, hasInitData = false) {
if (!hasInitData) { if (!hasInitData) {
this.stateService.isLoadingWebSocket$.next(true); this.stateService.isLoadingWebSocket$.next(true);
@ -237,7 +242,7 @@ export class WebsocketService {
this.goneOffline = true; this.goneOffline = true;
this.stateService.connectionState$.next(0); this.stateService.connectionState$.next(0);
window.setTimeout(() => { window.setTimeout(() => {
this.startSubscription(true); this.reconnectWebsocket(true);
}, retryDelay); }, retryDelay);
} }

View File

@ -2,12 +2,12 @@ import { Component, Input, OnInit } from "@angular/core";
import { DomSanitizer, SafeHtml } from "@angular/platform-browser"; import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
const MempoolErrors = { const MempoolErrors = {
'internal_server_error': `Something went wrong, please try again later`,
'acceleration_duplicated': `This transaction has already been accelerated.`, 'acceleration_duplicated': `This transaction has already been accelerated.`,
'acceleration_outbid': `Your fee delta is too low.`, 'acceleration_outbid': `Your fee delta is too low.`,
'cannot_accelerate_tx': `Cannot accelerate this transaction.`, 'cannot_accelerate_tx': `Cannot accelerate this transaction.`,
'cannot_decode_raw_tx': `Cannot decode this raw transaction.`, 'cannot_decode_raw_tx': `Cannot decode this raw transaction.`,
'cannot_fetch_raw_tx': `Cannot find this transaction.`, 'cannot_fetch_raw_tx': `Cannot find this transaction.`,
'database_error': `Something went wrong. Please try again later.`,
'high_sigop_tx': `This transaction cannot be accelerated.`, 'high_sigop_tx': `This transaction cannot be accelerated.`,
'invalid_acceleration_request': `This acceleration request is not valid.`, 'invalid_acceleration_request': `This acceleration request is not valid.`,
'invalid_tx_dependencies': `This transaction dependencies are not valid.`, 'invalid_tx_dependencies': `This transaction dependencies are not valid.`,

View File

@ -1698,7 +1698,7 @@
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="meta.description.bitcoin.address" datatype="html"> <trans-unit id="meta.description.bitcoin.address" datatype="html">
<source>See mempool transactions, confirmed transactions, balance, and more for <x id="PH" equiv-text="this.stateService.network===&apos;liquid&apos;||this.stateService.network===&apos;liquidtestnet&apos;?&apos;Liquid&apos;:&apos;Bitcoin&apos;"/><x id="PH_1" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> address <x id="INTERPOLATION" equiv-text="this.addressString"/>.</source> <source>See mempool transactions, confirmed transactions, balance, and more for <x id="PH" equiv-text="this.stateService.network===&apos;liquid&apos;||this.stateService.network===&apos;liquidtestnet&apos;?&apos;Liquid&apos;:&apos;Bitcoin&apos;"/> <x id="PH_1" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> address <x id="INTERPOLATION" equiv-text="this.addressString"/>.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/address/address-preview.component.ts</context> <context context-type="sourcefile">src/app/components/address/address-preview.component.ts</context>
<context context-type="linenumber">72</context> <context context-type="linenumber">72</context>
@ -2256,7 +2256,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts</context>
<context context-type="linenumber">215</context> <context context-type="linenumber">216</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-map/nodes-map.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-map/nodes-map.component.ts</context>
@ -3774,7 +3774,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-channels-map/nodes-channels-map.component.html</context> <context context-type="sourcefile">src/app/lightning/nodes-channels-map/nodes-channels-map.component.html</context>
<context context-type="linenumber">6</context> <context context-type="linenumber">19</context>
</context-group> </context-group>
<note priority="1" from="description">lightning.nodes-channels-world-map</note> <note priority="1" from="description">lightning.nodes-channels-world-map</note>
</trans-unit> </trans-unit>
@ -5024,7 +5024,7 @@
<note priority="1" from="description">transactions-list.coinbase</note> <note priority="1" from="description">transactions-list.coinbase</note>
</trans-unit> </trans-unit>
<trans-unit id="meta.description.bitcoin.transaction" datatype="html"> <trans-unit id="meta.description.bitcoin.transaction" datatype="html">
<source>Get real-time status, addresses, fees, script info, and more for <x id="PH" equiv-text="this.stateService.network===&apos;liquid&apos;||this.stateService.network===&apos;liquidtestnet&apos;?&apos;Liquid&apos;:&apos;Bitcoin&apos;"/><x id="PH_1" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> transaction with txid {txid}.</source> <source>Get real-time status, addresses, fees, script info, and more for <x id="PH" equiv-text="this.stateService.network===&apos;liquid&apos;||this.stateService.network===&apos;liquidtestnet&apos;?&apos;Liquid&apos;:&apos;Bitcoin&apos;"/><x id="PH_1" equiv-text="seoDescriptionNetwork(this.stateService.network)"/> transaction with txid <x id="PH_2" equiv-text="this.txId"/>.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/transaction/transaction-preview.component.ts</context> <context context-type="sourcefile">src/app/components/transaction/transaction-preview.component.ts</context>
<context context-type="linenumber">91</context> <context context-type="linenumber">91</context>
@ -6629,7 +6629,7 @@
<note priority="1" from="description">lightning.connectivity-ranking</note> <note priority="1" from="description">lightning.connectivity-ranking</note>
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.dashboard" datatype="html"> <trans-unit id="meta.description.lightning.dashboard" datatype="html">
<source>Get stats on the Lightning network (aggregate capacity, connectivity, etc) and Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc).</source> <source>Get stats on the Lightning network (aggregate capacity, connectivity, etc), Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts</context> <context context-type="sourcefile">src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts</context>
<context context-type="linenumber">28</context> <context context-type="linenumber">28</context>
@ -6886,7 +6886,7 @@
<source>(Tor nodes excluded)</source> <source>(Tor nodes excluded)</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-channels-map/nodes-channels-map.component.html</context> <context context-type="sourcefile">src/app/lightning/nodes-channels-map/nodes-channels-map.component.html</context>
<context context-type="linenumber">8</context> <context context-type="linenumber">21</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-map/nodes-map.component.html</context> <context context-type="sourcefile">src/app/lightning/nodes-map/nodes-map.component.html</context>
@ -6906,21 +6906,21 @@
<source>Lightning Nodes Channels World Map</source> <source>Lightning Nodes Channels World Map</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts</context>
<context context-type="linenumber">68</context> <context context-type="linenumber">69</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.node-map" datatype="html"> <trans-unit id="meta.description.lightning.node-map" datatype="html">
<source>See the channels of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details.</source> <source>See the channels of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts</context>
<context context-type="linenumber">69</context> <context context-type="linenumber">70</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4390631969351833104" datatype="html"> <trans-unit id="4390631969351833104" datatype="html">
<source>No geolocation data available</source> <source>No geolocation data available</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts</context>
<context context-type="linenumber">227</context> <context context-type="linenumber">228</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="a4d393ee035f4225083c22cc3909b26a05a87528" datatype="html"> <trans-unit id="a4d393ee035f4225083c22cc3909b26a05a87528" datatype="html">
@ -7289,7 +7289,7 @@
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="meta.description.lightning.rankings-dashboard" datatype="html"> <trans-unit id="meta.description.lightning.rankings-dashboard" datatype="html">
<source>See top the Lightning network nodes ranked by liquidity, connectivity, and age.</source> <source>See the top Lightning network nodes ranked by liquidity, connectivity, and age.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts</context> <context context-type="sourcefile">src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts</context>
<context context-type="linenumber">23</context> <context context-type="linenumber">23</context>