Compare commits

..

1 Commits

Author SHA1 Message Date
hunicus
9522d6ab0a Add note about enterprise support to readmes 2023-04-04 02:00:36 -04:00
33 changed files with 127 additions and 235 deletions

View File

@@ -30,6 +30,8 @@ Mempool can be conveniently installed on the following full-node distros:
Mempool can be installed in other ways too, but we only recommend doing so if you're a developer, have experience managing servers, or otherwise know what you're doing.
**We only provide support for advanced installation methods to <a href="https://mempool.space/enterprise">Enterprise sponsors</a>.**
- See the [`docker/`](./docker/) directory for instructions on deploying Mempool with Docker.
- See the [`backend/`](./backend/) and [`frontend/`](./frontend/) directories for manual install instructions oriented for developers.
- See the [`production/`](./production/) directory for guidance on setting up a more serious Mempool instance designed for high performance at scale.

View File

@@ -2,7 +2,9 @@
These instructions are mostly intended for developers.
If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing. Mempool does not provide support for custom setups.
If you choose to use these instructions for a production setup, be aware that you will still probably need to do additional configuration for your specific OS, environment, use-case, etc. We do our best here to provide a good starting point, but only proceed if you know what you're doing.
**We only provide support for this installation method to <a href="https://mempool.space/enterprise">Enterprise sponsors</a>.**
See other ways to set up Mempool on [the main README](/../../#installation-methods).

View File

@@ -3,102 +3,68 @@ import axios, { AxiosRequestConfig } from 'axios';
import http from 'http';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger';
const axiosConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true, })
});
class ElectrsApi implements AbstractBitcoinApi {
private axiosConfigWithUnixSocket: AxiosRequestConfig = config.ESPLORA.UNIX_SOCKET_PATH ? {
axiosConfig: AxiosRequestConfig = config.ESPLORA.UNIX_SOCKET_PATH ? {
socketPath: config.ESPLORA.UNIX_SOCKET_PATH,
timeout: 10000,
} : {
timeout: 10000,
};
private axiosConfigTcpSocketOnly: AxiosRequestConfig = {
timeout: 10000,
};
unixSocketRetryTimeout;
activeAxiosConfig;
constructor() {
this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
}
fallbackToTcpSocket() {
if (!this.unixSocketRetryTimeout) {
logger.err(`Unable to connect to esplora unix socket. Falling back to tcp socket. Retrying unix socket in ${config.ESPLORA.RETRY_UNIX_SOCKET_AFTER / 1000} seconds`);
// Retry the unix socket after a few seconds
this.unixSocketRetryTimeout = setTimeout(() => {
logger.info(`Retrying to use unix socket for esplora now (applied for the next query)`);
this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
this.unixSocketRetryTimeout = undefined;
}, config.ESPLORA.RETRY_UNIX_SOCKET_AFTER);
}
// Use the TCP socket (reach a different esplora instance through nginx)
this.activeAxiosConfig = this.axiosConfigTcpSocketOnly;
}
$queryWrapper<T>(url, responseType = 'json'): Promise<T> {
return axiosConnection.get<T>(url, { ...this.activeAxiosConfig, responseType: responseType })
.then((response) => response.data)
.catch((e) => {
if (e?.code === 'ECONNREFUSED') {
this.fallbackToTcpSocket();
// Retry immediately
return axiosConnection.get<T>(url, this.activeAxiosConfig)
.then((response) => response.data)
.catch((e) => {
logger.warn(`Cannot query esplora through the unix socket nor the tcp socket. Exception ${e}`);
throw e;
});
} else {
throw e;
}
});
}
constructor() { }
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
return this.$queryWrapper<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids');
return axiosConnection.get<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids', this.axiosConfig)
.then((response) => response.data);
}
$getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
return this.$queryWrapper<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId);
return axiosConnection.get<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig)
.then((response) => response.data);
}
$getTransactionHex(txId: string): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex');
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex', this.axiosConfig)
.then((response) => response.data);
}
$getBlockHeightTip(): Promise<number> {
return this.$queryWrapper<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height');
return axiosConnection.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig)
.then((response) => response.data);
}
$getBlockHashTip(): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash');
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash', this.axiosConfig)
.then((response) => response.data);
}
$getTxIdsForBlock(hash: string): Promise<string[]> {
return this.$queryWrapper<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids');
return axiosConnection.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig)
.then((response) => response.data);
}
$getBlockHash(height: number): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height);
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height, this.axiosConfig)
.then((response) => response.data);
}
$getBlockHeader(hash: string): Promise<string> {
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header');
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header', this.axiosConfig)
.then((response) => response.data);
}
$getBlock(hash: string): Promise<IEsploraApi.Block> {
return this.$queryWrapper<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash);
return axiosConnection.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig)
.then((response) => response.data);
}
$getRawBlock(hash: string): Promise<Buffer> {
return this.$queryWrapper<any>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", 'arraybuffer')
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", { ...this.axiosConfig, responseType: 'arraybuffer' })
.then((response) => { return Buffer.from(response.data); });
}
@@ -119,11 +85,13 @@ class ElectrsApi implements AbstractBitcoinApi {
}
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return this.$queryWrapper<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout);
return axiosConnection.get<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig)
.then((response) => response.data);
}
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
return this.$queryWrapper<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends');
return axiosConnection.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig)
.then((response) => response.data);
}
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {

View File

@@ -857,7 +857,6 @@ class Blocks {
}
if (cleanBlock.fee_amt_percentiles !== null) {
cleanBlock.median_fee_amt = cleanBlock.fee_amt_percentiles[3];
await blocksRepository.$updateFeeAmounts(cleanBlock.hash, cleanBlock.fee_amt_percentiles, cleanBlock.median_fee_amt);
}
}

View File

@@ -2,7 +2,7 @@ import * as fs from 'fs';
import logger from '../../logger';
class Icons {
private static FILE_NAME = '/elements/asset_registry_db/icons.json';
private static FILE_NAME = './icons.json';
private iconIds: string[] = [];
private icons: { [assetId: string]: string; } = {};

View File

@@ -38,7 +38,6 @@ interface IConfig {
ESPLORA: {
REST_API_URL: string;
UNIX_SOCKET_PATH: string | void | null;
RETRY_UNIX_SOCKET_AFTER: number;
};
LIGHTNING: {
ENABLED: boolean;
@@ -166,7 +165,6 @@ const defaults: IConfig = {
'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000',
'UNIX_SOCKET_PATH': null,
'RETRY_UNIX_SOCKET_AFTER': 30000,
},
'ELECTRUM': {
'HOST': '127.0.0.1',

View File

@@ -45,8 +45,7 @@ class Server {
private wss: WebSocket.Server | undefined;
private server: http.Server | undefined;
private app: Application;
private currentBackendRetryInterval = 1;
private backendRetryCount = 0;
private currentBackendRetryInterval = 5;
private maxHeapSize: number = 0;
private heapLogInterval: number = 60;
@@ -185,17 +184,17 @@ class Server {
indexer.$run();
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
this.backendRetryCount = 0;
this.currentBackendRetryInterval = 5;
} catch (e: any) {
this.backendRetryCount++;
let loggerMsg = `Exception in runMainUpdateLoop() (count: ${this.backendRetryCount}). Retrying in ${this.currentBackendRetryInterval} sec.`;
let loggerMsg = `Exception in runMainUpdateLoop(). Retrying in ${this.currentBackendRetryInterval} sec.`;
loggerMsg += ` Reason: ${(e instanceof Error ? e.message : e)}.`;
if (e?.stack) {
loggerMsg += ` Stack trace: ${e.stack}`;
}
// When we get a first Exception, only `logger.debug` it and retry after 5 seconds
// From the second Exception, `logger.warn` the Exception and increase the retry delay
if (this.backendRetryCount >= 5) {
// Maximum retry delay is 60 seconds
if (this.currentBackendRetryInterval > 5) {
logger.warn(loggerMsg);
mempool.setOutOfSync();
} else {
@@ -205,6 +204,8 @@ class Server {
logger.debug(`AxiosError: ${e?.message}`);
}
setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval);
this.currentBackendRetryInterval *= 2;
this.currentBackendRetryInterval = Math.min(this.currentBackendRetryInterval, 60);
}
}

View File

@@ -13,48 +13,6 @@ import chainTips from '../api/chain-tips';
import blocks from '../api/blocks';
import BlocksAuditsRepository from './BlocksAuditsRepository';
interface DatabaseBlock {
id: string;
height: number;
version: number;
timestamp: number;
bits: number;
nonce: number;
difficulty: number;
merkle_root: string;
tx_count: number;
size: number;
weight: number;
previousblockhash: string;
mediantime: number;
totalFees: number;
medianFee: number;
feeRange: string;
reward: number;
poolId: number;
poolName: string;
poolSlug: string;
avgFee: number;
avgFeeRate: number;
coinbaseRaw: string;
coinbaseAddress: string;
coinbaseSignature: string;
coinbaseSignatureAscii: string;
avgTxSize: number;
totalInputs: number;
totalOutputs: number;
totalOutputAmt: number;
medianFeeAmt: number;
feePercentiles: string;
segwitTotalTxs: number;
segwitTotalSize: number;
segwitTotalWeight: number;
header: string;
utxoSetChange: number;
utxoSetSize: number;
totalInputAmt: number;
}
const BLOCK_DB_FIELDS = `
blocks.hash AS id,
blocks.height,
@@ -94,7 +52,7 @@ const BLOCK_DB_FIELDS = `
blocks.header,
blocks.utxoset_change AS utxoSetChange,
blocks.utxoset_size AS utxoSetSize,
blocks.total_input_amt AS totalInputAmt
blocks.total_input_amt AS totalInputAmts
`;
class BlocksRepository {
@@ -213,32 +171,6 @@ class BlocksRepository {
}
}
/**
* Update missing fee amounts fields
*
* @param blockHash
* @param feeAmtPercentiles
* @param medianFeeAmt
*/
public async $updateFeeAmounts(blockHash: string, feeAmtPercentiles, medianFeeAmt) : Promise<void> {
try {
const query = `
UPDATE blocks
SET fee_percentiles = ?, median_fee_amt = ?
WHERE hash = ?
`;
const params: any[] = [
JSON.stringify(feeAmtPercentiles),
medianFeeAmt,
blockHash
];
await DB.query(query, params);
} catch (e: any) {
logger.err(`Cannot update fee amounts for block ${blockHash}. Reason: ' + ${e instanceof Error ? e.message : e}`);
throw e;
}
}
/**
* Get all block height that have not been indexed between [startHeight, endHeight]
*/
@@ -500,7 +432,7 @@ class BlocksRepository {
const blocks: BlockExtended[] = [];
for (const block of rows) {
blocks.push(await this.formatDbBlockIntoExtendedBlock(block as DatabaseBlock));
blocks.push(await this.formatDbBlockIntoExtendedBlock(block));
}
return blocks;
@@ -527,7 +459,7 @@ class BlocksRepository {
return null;
}
return await this.formatDbBlockIntoExtendedBlock(rows[0] as DatabaseBlock);
return await this.formatDbBlockIntoExtendedBlock(rows[0]);
} catch (e) {
logger.err(`Cannot get indexed block ${height}. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
@@ -976,7 +908,7 @@ class BlocksRepository {
*
* @param dbBlk
*/
private async formatDbBlockIntoExtendedBlock(dbBlk: DatabaseBlock): Promise<BlockExtended> {
private async formatDbBlockIntoExtendedBlock(dbBlk: any): Promise<BlockExtended> {
const blk: Partial<BlockExtended> = {};
const extras: Partial<BlockExtension> = {};
@@ -1048,12 +980,11 @@ class BlocksRepository {
if (extras.feePercentiles === null) {
const block = await bitcoinClient.getBlock(dbBlk.id, 2);
const summary = blocks.summarizeBlock(block);
await BlocksSummariesRepository.$saveTransactions(dbBlk.height, dbBlk.id, summary.transactions);
await BlocksSummariesRepository.$saveTransactions(dbBlk.height, dbBlk.hash, summary.transactions);
extras.feePercentiles = await BlocksSummariesRepository.$getFeePercentilesByBlockId(dbBlk.id);
}
if (extras.feePercentiles !== null) {
extras.medianFeeAmt = extras.feePercentiles[3];
await this.$updateFeeAmounts(dbBlk.id, extras.feePercentiles, extras.medianFeeAmt);
}
}

View File

@@ -17,6 +17,26 @@ class BlocksSummariesRepository {
return undefined;
}
public async $saveSummary(params: { height: number, mined?: BlockSummary}): Promise<void> {
const blockId = params.mined?.id;
try {
const transactions = JSON.stringify(params.mined?.transactions || []);
await DB.query(`
INSERT INTO blocks_summaries (height, id, transactions, template)
VALUE (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
transactions = ?
`, [params.height, blockId, transactions, '[]', transactions]);
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save block summary for ${blockId} because it has already been indexed, ignoring`);
} else {
logger.debug(`Cannot save block summary for ${blockId}. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
}
public async $saveTransactions(blockHeight: number, blockId: string, transactions: TransactionStripped[]): Promise<void> {
try {
const transactionsStr = JSON.stringify(transactions);

View File

@@ -6,6 +6,8 @@ If you are looking to use these Docker images to deploy your own instance of Mem
See a video guide of this installation method by k3tan [on BitcoinTV.com](https://bitcointv.com/w/8fpAx6rf5CQ16mMhospwjg).
**We only provide support for this installation method to <a href="https://mempool.space/enterprise">Enterprise sponsors</a>.**
Jump to a section in this doc:
- [Configure with Bitcoin Core Only](#configure-with-bitcoin-core-only)
- [Configure with Bitcoin Core + Electrum Server](#configure-with-bitcoin-core--electrum-server)

View File

@@ -22,7 +22,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
@Input() offset: number = 0;
@Input() height: number = 0; // max height of blocks in chunk (dynamic blocks only)
@Input() count: number = 8; // number of blocks in this chunk (dynamic blocks only)
@Input() dynamicBlockCount: number = 8; // number of blocks in the dynamic block chunk
@Input() loadingTip: boolean = false;
@Input() connected: boolean = true;
@@ -46,6 +45,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
feeRounding = '1.0-0';
arrowVisible = false;
arrowLeftPx = 30;
blocksFilled = false;
arrowTransition = '1s';
showMiningInfo = false;
timeLtrSubscription: Subscription;
@@ -96,13 +96,14 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.tabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
if (!this.static) {
this.blocksSubscription = this.stateService.blocks$
.subscribe(([block, txConfirmed, batch]) => {
.subscribe(([block, txConfirmed]) => {
if (this.blocks.some((b) => b.height === block.height)) {
return;
}
if (this.blocks.length && block.height !== this.blocks[0].height + 1) {
this.blocks = [];
this.blocksFilled = false;
}
this.blocks.unshift(block);
@@ -116,18 +117,20 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
}
this.blockStyles = [];
this.blocks.forEach((b, i) => {
if (i === 0 && !batch && block.height > this.chainTip) {
this.blockStyles.push(this.getStyleForBlock(b, i, -205));
setTimeout(() => {
this.blockStyles = [];
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
this.cd.markForCheck();
}, 50);
} else {
this.blockStyles.push(this.getStyleForBlock(b, i));
}
});
if (this.blocksFilled && block.height > this.chainTip) {
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205)));
setTimeout(() => {
this.blockStyles = [];
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
this.cd.markForCheck();
}, 50);
} else {
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
}
if (this.blocks.length === this.dynamicBlocksAmount) {
this.blocksFilled = true;
}
this.chainTip = Math.max(this.chainTip, block.height);
this.cd.markForCheck();
@@ -157,7 +160,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges): void {
if (this.static) {
const animateSlide = (changes.dynamicBlockCount && changes.dynamicBlockCount.previousValue != null) || (changes.height && (changes.height.currentValue === changes.height.previousValue + 1));
const animateSlide = changes.height && (changes.height.currentValue === changes.height.previousValue + 1);
this.updateStaticBlocks(animateSlide);
}
}
@@ -224,8 +227,8 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
block = this.cacheService.getCachedBlock(height) || null;
}
this.blocks.push(block || {
placeholder: !isNaN(height) && height < 0,
loading: isNaN(height) || height >= 0,
placeholder: height < 0,
loading: height >= 0,
id: '',
height,
version: 0,
@@ -308,7 +311,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
return {
left: addLeft + (155 * index) + 'px',
background: "#2d3348",
transition: animateEnterFrom ? 'background 2s, transform 1s' : null,
};
}
@@ -316,7 +318,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
const addLeft = animateEnterFrom || 0;
return {
left: addLeft + (155 * index) + 'px',
transition: animateEnterFrom ? 'background 2s, transform 1s' : null,
};
}
@@ -325,7 +326,6 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
return {
left: addLeft + 155 * this.emptyBlocks.indexOf(block) + 'px',
transition: animateEnterFrom ? 'background 2s, transform 1s' : null,
background: "#2d3348",
};
}

View File

@@ -6,7 +6,7 @@
<app-mempool-blocks [hidden]="pageIndex > 0"></app-mempool-blocks>
<app-blockchain-blocks [hidden]="pageIndex > 0"></app-blockchain-blocks>
<ng-container *ngFor="let page of pages; trackBy: trackByPageFn">
<app-blockchain-blocks [static]="true" [offset]="page.offset" [height]="page.height" [dynamicBlockCount]="dynamicBlockCount" [count]="blocksPerPage" [loadingTip]="loadingTip" [connected]="connected"></app-blockchain-blocks>
<app-blockchain-blocks [static]="true" [offset]="page.offset" [height]="page.height" [count]="blocksPerPage" [loadingTip]="loadingTip" [connected]="connected"></app-blockchain-blocks>
</ng-container>
</div>
<div id="divider" [hidden]="pageIndex > 0">

View File

@@ -12,7 +12,6 @@ export class BlockchainComponent implements OnInit, OnDestroy {
@Input() pages: any[] = [];
@Input() pageIndex: number;
@Input() blocksPerPage: number = 8;
@Input() dynamicBlockCount: number = 8;
@Input() minScrollWidth: number = 0;
network: string;

View File

@@ -105,6 +105,7 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
});
this.reduceMempoolBlocksToFitScreen(this.mempoolBlocks);
this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
this.loadingBlocks$ = this.stateService.isLoadingWebSocket$;
this.mempoolBlocks$ = merge(
of(true),
@@ -140,13 +141,6 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
})
);
this.loadingBlocks$ = combineLatest([
this.stateService.isLoadingWebSocket$,
this.mempoolBlocks$
]).pipe(map(([loading, mempoolBlocks]) => {
return loading || !mempoolBlocks.length;
}));
this.difficultyAdjustments$ = this.stateService.difficultyAdjustment$
.pipe(
map((da) => {

View File

@@ -16,7 +16,7 @@
(dragstart)="onDragStart($event)"
(scroll)="onScroll($event)"
>
<app-blockchain [pageIndex]="pageIndex" [pages]="pages" [dynamicBlockCount]="dynamicBlocksAmount" [blocksPerPage]="blocksPerPage" [minScrollWidth]="minScrollWidth"></app-blockchain>
<app-blockchain [pageIndex]="pageIndex" [pages]="pages" [blocksPerPage]="blocksPerPage" [minScrollWidth]="minScrollWidth"></app-blockchain>
</div>
<div class="reset-scroll" [class.hidden]="pageIndex === 0" (click)="resetScroll()">
<fa-icon [icon]="['fas', 'circle-left']" [fixedWidth]="true"></fa-icon>

View File

@@ -2,7 +2,6 @@ import { Component, ElementRef, HostListener, OnInit, OnDestroy, ViewChild } fro
import { Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
import { specialBlocks } from '../../app.constants';
import { BlockExtended } from '../../interfaces/node-api.interface';
@Component({
selector: 'app-start',
@@ -30,8 +29,7 @@ export class StartComponent implements OnInit, OnDestroy {
isMobile: boolean = false;
isiOS: boolean = false;
blockWidth = 155;
blocks: BlockExtended[] = [];
dynamicBlocksAmount: number;
dynamicBlocksAmount: number = 8;
blockCount: number = 0;
blocksPerPage: number = 1;
pageWidth: number;
@@ -51,28 +49,15 @@ export class StartComponent implements OnInit, OnDestroy {
}
ngOnInit() {
this.firstPageWidth = 40 + (this.blockWidth * (this.dynamicBlocksAmount || 0));
this.blockCounterSubscription = this.stateService.blocks$.subscribe(([block, txConfirmed, batch]) => {
if (this.blocks.some((b) => b.height === block.height)) {
return;
}
if (this.blocks.length && block.height !== this.blocks[0].height + 1) {
this.blocks = [];
}
this.blocks.unshift(block);
this.blocks = this.blocks.slice(0, Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT));
this.dynamicBlocksAmount = this.blocks.length;
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
this.blockCounterSubscription = this.stateService.blocks$.subscribe(() => {
this.blockCount++;
this.dynamicBlocksAmount = Math.min(this.blockCount, this.stateService.env.KEEP_BLOCKS_AMOUNT, 8);
this.firstPageWidth = 40 + (this.blockWidth * this.dynamicBlocksAmount);
if (this.blocks.length <= Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT)) {
if (this.blockCount <= Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT)) {
this.onResize();
}
});
this.onResize();
this.updatePages();
this.timeLtrSubscription = this.stateService.timeLtr.subscribe((ltr) => {

View File

@@ -89,7 +89,7 @@ export class StateService {
networkChanged$ = new ReplaySubject<string>(1);
lightningChanged$ = new ReplaySubject<boolean>(1);
blocks$: ReplaySubject<[BlockExtended, boolean, boolean]>;
blocks$: ReplaySubject<[BlockExtended, boolean]>;
transactions$ = new ReplaySubject<TransactionStripped>(6);
conversions$ = new ReplaySubject<any>(1);
bsqPrice$ = new ReplaySubject<number>(1);
@@ -157,7 +157,7 @@ export class StateService {
}
});
this.blocks$ = new ReplaySubject<[BlockExtended, boolean, boolean]>(this.env.KEEP_BLOCKS_AMOUNT);
this.blocks$ = new ReplaySubject<[BlockExtended, boolean]>(this.env.KEEP_BLOCKS_AMOUNT);
if (this.env.BASE_MODULE === 'bisq') {
this.network = this.env.BASE_MODULE;

View File

@@ -228,7 +228,7 @@ export class WebsocketService {
blocks.forEach((block: BlockExtended) => {
if (block.height > this.stateService.latestBlockHeight) {
maxHeight = Math.max(maxHeight, block.height);
this.stateService.blocks$.next([block, false, true]);
this.stateService.blocks$.next([block, false]);
}
});
this.stateService.updateChainTip(maxHeight);
@@ -241,7 +241,7 @@ export class WebsocketService {
if (response.block) {
if (response.block.height > this.stateService.latestBlockHeight) {
this.stateService.updateChainTip(response.block.height);
this.stateService.blocks$.next([response.block, !!response.txConfirmed, false]);
this.stateService.blocks$.next([response.block, !!response.txConfirmed]);
}
if (response.txConfirmed) {

View File

@@ -2,7 +2,7 @@
These instructions are for setting up a serious production Mempool website for Bitcoin (mainnet, testnet, signet), Liquid (mainnet, testnet), and Bisq.
Again, this setup is no joke—home users should use [one of the other installation methods](../#installation-methods).
This setup is no joke—home users should use [one of the other installation methods](../#installation-methods). We only provide support for this installation method to <a href="https://mempool.space/enterprise">Enterprise sponsors</a>.
### Server Hardware

View File

@@ -1,5 +1,5 @@
@reboot screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet
@reboot /usr/local/bin/bitcoind -testnet >/dev/null 2>&1
@reboot screen -dmS testnet /bitcoin/electrs/electrs-start-testnet
@reboot /usr/local/bin/bitcoind -signet >/dev/null 2>&1
@reboot screen -dmS signet /bitcoin/electrs/electrs-start-signet
@reboot sleep 30 ; screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet
@reboot sleep 60 ; /usr/local/bin/bitcoind -testnet >/dev/null 2>&1
@reboot sleep 70 ; screen -dmS testnet /bitcoin/electrs/electrs-start-testnet
@reboot sleep 80 ; /usr/local/bin/bitcoind -signet >/dev/null 2>&1
@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet

View File

@@ -1,10 +1,10 @@
# start elements on reboot
@reboot /usr/local/bin/elementsd -chain=liquidv1 >/dev/null 2>&1
@reboot /usr/local/bin/elementsd -chain=liquidtestnet >/dev/null 2>&1
@reboot sleep 60 ; /usr/local/bin/elementsd -chain=liquidv1 >/dev/null 2>&1
@reboot sleep 60 ; /usr/local/bin/elementsd -chain=liquidtestnet >/dev/null 2>&1
# start electrs on reboot
@reboot screen -dmS liquidv1 /elements/electrs/electrs-start-liquid
@reboot screen -dmS liquidtestnet /elements/electrs/electrs-start-liquidtestnet
@reboot sleep 90 ; screen -dmS liquidv1 /elements/electrs/electrs-start-liquid
@reboot sleep 90 ; screen -dmS liquidtestnet /elements/electrs/electrs-start-liquidtestnet
# hourly asset update and electrs restart
6 * * * * cd $HOME/asset_registry_db && git pull --quiet origin master && cd $HOME/asset_registry_testnet_db && git pull --quiet origin master && killall electrs

View File

@@ -1325,10 +1325,10 @@ case $OS in
public_ipv4=$( ifconfig | grep 'inet ' | awk -F ' ' '{ print $2 }' | grep -v '^103\.165\.192\.' | grep -v '^127\.0\.0\.1' )
public_ipv6=$( ifconfig | grep 'inet6' | awk -F ' ' '{ print $2 }' | grep -v '^2001:df6:7280::' | grep -v '^fe80::' | grep -v '^::1' )
crontab_cln+="@reboot sleep 10 ; screen -dmS main lightningd --rpc-file-mode 0660 --alias `hostname` --disable-ip-discovery --autolisten false --bind-addr $public_ipv4 --announce-addr $public_ipv4 --bind-addr $public_ipv6 --announce-addr $public_ipv6\n"
crontab_cln+="@reboot sleep 10 ; screen -dmS tes lightningd --rpc-file-mode 0660 --alias `hostname` --network testnet --disable-ip-discovery --autolisten false --bind-addr $public_ipv4 --announce-addr $public_ipv4 --bind-addr $public_ipv6 --announce-addr $public_ipv6\n"
crontab_cln+="@reboot sleep 10 ; screen -dmS sig lightningd --rpc-file-mode 0660 --alias `hostname` --network signet --disable-ip-discovery --autolisten false --bind-addr $public_ipv4 --announce-addr $public_ipv4 --bind-addr $public_ipv6 --announce-addr $public_ipv6 \n"
crontab_cln+="@reboot sleep 20 ; /mempool/mempool.space/lightning-seeder >/dev/null 2>&1\n"
crontab_cln+="@reboot sleep 60 ; screen -dmS main lightningd --rpc-file-mode 0660 --alias `hostname` --disable-ip-discovery --autolisten false --bind-addr $public_ipv4 --announce-addr $public_ipv4 --bind-addr $public_ipv6 --announce-addr $public_ipv6\n"
crontab_cln+="@reboot sleep 90 ; screen -dmS tes lightningd --rpc-file-mode 0660 --alias `hostname` --network testnet --disable-ip-discovery --autolisten false --bind-addr $public_ipv4 --announce-addr $public_ipv4 --bind-addr $public_ipv6 --announce-addr $public_ipv6\n"
crontab_cln+="@reboot sleep 120 ; screen -dmS sig lightningd --rpc-file-mode 0660 --alias `hostname` --network signet --disable-ip-discovery --autolisten false --bind-addr $public_ipv4 --announce-addr $public_ipv4 --bind-addr $public_ipv6 --announce-addr $public_ipv6 \n"
crontab_cln+="@reboot sleep 180 ; /mempool/mempool.space/lightning-seeder >/dev/null 2>&1\n"
crontab_cln+="1 * * * * /mempool/mempool.space/lightning-seeder >/dev/null 2>&1\n"
echo "${crontab_cln}" | crontab -u "${CLN_USER}" -
;;

View File

@@ -15,13 +15,12 @@
"PASSWORD": "__BITCOIN_RPC_PASS__"
},
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:5000",
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet"
"REST_API_URL": "http://127.0.0.1:4000"
},
"DATABASE": {
"ENABLED": false,
"HOST": "127.0.0.1",
"SOCKET": "/var/run/mysql/mysql.sock",
"PORT": 3306,
"USERNAME": "__MEMPOOL_BISQ_USER__",
"PASSWORD": "__MEMPOOL_BISQ_PASS__",
"DATABASE": "mempool_bisq"

View File

@@ -23,13 +23,12 @@
"PASSWORD": "__BITCOIN_RPC_PASS__"
},
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:5001",
"UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-mainnet"
},
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",
"SOCKET": "/var/run/mysql/mysql.sock",
"PORT": 3306,
"USERNAME": "__MEMPOOL_LIQUID_USER__",
"PASSWORD": "__MEMPOOL_LIQUID_PASS__",
"DATABASE": "mempool_liquid"

View File

@@ -23,13 +23,12 @@
"PASSWORD": "__BITCOIN_RPC_PASS__"
},
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:5004",
"UNIX_SOCKET_PATH": "/elements/socket/esplora-liquid-testnet"
},
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",
"SOCKET": "/var/run/mysql/mysql.sock",
"PORT": 3306,
"USERNAME": "__MEMPOOL_LIQUIDTESTNET_USER__",
"PASSWORD": "__MEMPOOL_LIQUIDTESTNET_PASS__",
"DATABASE": "mempool_liquidtestnet"

View File

@@ -15,8 +15,7 @@
"PASSWORD": "__BITCOIN_RPC_PASS__"
},
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:5000",
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet"
"REST_API_URL": "http://127.0.0.1:4000"
},
"LIGHTNING": {
"ENABLED": true,
@@ -42,7 +41,7 @@
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",
"SOCKET": "/var/run/mysql/mysql.sock",
"PORT": 3306,
"USERNAME": "__MEMPOOL_MAINNET_LIGHTNING_USER__",
"PASSWORD": "__MEMPOOL_MAINNET_LIGHTNING_PASS__",
"DATABASE": "mempool_mainnet_lightning"

View File

@@ -31,13 +31,12 @@
"PASSWORD": "__BITCOIN_RPC_PASS__"
},
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:5000",
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-mainnet"
},
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",
"SOCKET": "/var/run/mysql/mysql.sock",
"PORT": 3306,
"USERNAME": "__MEMPOOL_MAINNET_USER__",
"PASSWORD": "__MEMPOOL_MAINNET_PASS__",
"DATABASE": "mempool"

View File

@@ -15,8 +15,7 @@
"PASSWORD": "__BITCOIN_RPC_PASS__"
},
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:5003",
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet"
"REST_API_URL": "http://127.0.0.1:4003"
},
"LIGHTNING": {
"ENABLED": true,
@@ -37,7 +36,7 @@
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",
"SOCKET": "/var/run/mysql/mysql.sock",
"PORT": 3306,
"USERNAME": "__MEMPOOL_SIGNET_LIGHTNING_USER__",
"PASSWORD": "__MEMPOOL_SIGNET_LIGHTNING_PASS__",
"DATABASE": "mempool_signet_lightning"

View File

@@ -22,13 +22,12 @@
"PASSWORD": "__BITCOIN_RPC_PASS__"
},
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:5003",
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-signet"
},
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",
"SOCKET": "/var/run/mysql/mysql.sock",
"PORT": 3306,
"USERNAME": "__MEMPOOL_SIGNET_USER__",
"PASSWORD": "__MEMPOOL_SIGNET_PASS__",
"DATABASE": "mempool_signet"

View File

@@ -15,8 +15,7 @@
"PASSWORD": "__BITCOIN_RPC_PASS__"
},
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:5002",
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet"
"REST_API_URL": "http://127.0.0.1:4002"
},
"LIGHTNING": {
"ENABLED": true,
@@ -37,7 +36,7 @@
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",
"SOCKET": "/var/run/mysql/mysql.sock",
"PORT": 3306,
"USERNAME": "__MEMPOOL_TESTNET_LIGHTNING_USER__",
"PASSWORD": "__MEMPOOL_TESTNET_LIGHTNING_PASS__",
"DATABASE": "mempool_testnet_lightning"

View File

@@ -22,13 +22,12 @@
"PASSWORD": "__BITCOIN_RPC_PASS__"
},
"ESPLORA": {
"REST_API_URL": "http://127.0.0.1:5002",
"UNIX_SOCKET_PATH": "/bitcoin/socket/esplora-bitcoin-testnet"
},
"DATABASE": {
"ENABLED": true,
"HOST": "127.0.0.1",
"SOCKET": "/var/run/mysql/mysql.sock",
"PORT": 3306,
"USERNAME": "__MEMPOOL_TESTNET_USER__",
"PASSWORD": "__MEMPOOL_TESTNET_PASS__",
"DATABASE": "mempool_testnet"

View File

@@ -1,5 +1,5 @@
# start on reboot
@reboot sleep 90 ; $HOME/start
@reboot sleep 10 ; $HOME/start
# daily backup
37 13 * * * sleep 30 ; /mempool/mempool.space/backup >/dev/null 2>&1 &

View File

@@ -1 +1 @@
@reboot /usr/local/bin/bitcoind >/dev/null 2>&1
@reboot sleep 120 ; /usr/local/bin/bitcoind >/dev/null 2>&1