Merge branch 'master' into qrcodes

This commit is contained in:
wiz 2022-07-06 23:31:01 +02:00 committed by GitHub
commit 98db8b1b25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
108 changed files with 7508 additions and 3026 deletions

View File

@ -1,48 +1,88 @@
name: CI Pipeline for the Backend and Frontend name: CI Pipeline for the Backend and Frontend
on: on:
push: push:
env: env:
NODE_VERSION: 16.15.0 NODE_VERSION: 16.15.0
jobs: jobs:
build_backend: backend:
name: Build backend strategy:
runs-on: ubuntu-latest matrix:
flavor: ['dev', 'prod']
runs-on: 'ubuntu-latest'
name: Backend (${{ matrix.flavor }})
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
path: ${{ matrix.flavor }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install
- name: Install
if: ${{ matrix.flavor == 'dev'}}
run: npm install
working-directory: ${{ matrix.flavor }}/backend
- name: Install (Prod dependencies only)
if: ${{ matrix.flavor == 'prod'}}
run: npm install --prod run: npm install --prod
working-directory: backend working-directory: ${{ matrix.flavor }}/backend
# - name: Lint
# run: npm run lint - name: Lint
if: ${{ matrix.flavor == 'dev'}}
run: npm run lint
working-directory: ${{ matrix.flavor }}/backend
# - name: Test # - name: Test
# run: npm run test # run: npm run test
- name: Build - name: Build
run: npm run build run: npm run build
working-directory: backend working-directory: ${{ matrix.flavor }}/backend
build_frontend:
name: Build frontend frontend:
runs-on: ubuntu-latest strategy:
matrix:
flavor: ['dev', 'prod']
runs-on: 'ubuntu-latest'
name: Frontend (${{ matrix.flavor }})
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
path: ${{ matrix.flavor }}
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install
run: npm install --prod - name: Install (Prod dependencies only)
working-directory: frontend run: npm install
# - name: Lint if: ${{ matrix.flavor == 'prod'}}
# run: npm run lint working-directory: ${{ matrix.flavor }}/frontend
- name: Install
if: ${{ matrix.flavor == 'dev'}}
run: npm install
working-directory: ${{ matrix.flavor }}/frontend
- name: Lint
run: npm run lint
working-directory: ${{ matrix.flavor }}/frontend
# - name: Test # - name: Test
# run: npm run test # run: npm run test
- name: Build - name: Build
run: npm run build run: npm run build
working-directory: frontend working-directory: ${{ matrix.flavor }}/frontend

2
backend/.eslintignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

32
backend/.eslintrc Normal file
View File

@ -0,0 +1,32 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/ban-ts-comment": 1,
"@typescript-eslint/ban-types": 1,
"@typescript-eslint/no-empty-function": 1,
"@typescript-eslint/no-explicit-any": 1,
"@typescript-eslint/no-inferrable-types": 1,
"@typescript-eslint/no-namespace": 1,
"@typescript-eslint/no-this-alias": 1,
"@typescript-eslint/no-var-requires": 1,
"no-console": 1,
"no-constant-condition": 1,
"no-dupe-else-if": 1,
"no-empty": 1,
"no-prototype-builtins": 1,
"no-self-assign": 1,
"no-useless-catch": 1,
"no-var": 1,
"prefer-const": 1,
"prefer-rest-params": 1
}
}

2633
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,9 @@
"build": "npm run tsc", "build": "npm run tsc",
"start": "node --max-old-space-size=2048 dist/index.js", "start": "node --max-old-space-size=2048 dist/index.js",
"start-production": "node --max-old-space-size=4096 dist/index.js", "start-production": "node --max-old-space-size=4096 dist/index.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1",
"lint": "./node_modules/.bin/eslint . --ext .ts",
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix"
}, },
"dependencies": { "dependencies": {
"@mempool/electrum-client": "^1.1.7", "@mempool/electrum-client": "^1.1.7",
@ -35,14 +37,17 @@
"express": "^4.18.0", "express": "^4.18.0",
"mysql2": "2.3.3", "mysql2": "2.3.3",
"node-worker-threads-pool": "^1.5.1", "node-worker-threads-pool": "^1.5.1",
"socks-proxy-agent": "^6.2.0", "socks-proxy-agent": "~7.0.0",
"typescript": "~4.7.2", "typescript": "~4.7.4",
"ws": "~8.7.0" "ws": "~8.8.0"
}, },
"devDependencies": { "devDependencies": {
"@types/compression": "^1.7.2", "@types/compression": "^1.7.2",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/ws": "~8.5.3", "@types/ws": "~8.5.3",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.5",
"eslint": "^8.19.0",
"tslint": "^6.1.0" "tslint": "^6.1.0"
} }
} }

View File

@ -4,6 +4,7 @@ export interface AbstractBitcoinApi {
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>; $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>; $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
$getBlockHeightTip(): Promise<number>; $getBlockHeightTip(): Promise<number>;
$getBlockHashTip(): Promise<string>;
$getTxIdsForBlock(hash: string): Promise<string[]>; $getTxIdsForBlock(hash: string): Promise<string[]>;
$getBlockHash(height: number): Promise<string>; $getBlockHash(height: number): Promise<string>;
$getBlockHeader(hash: string): Promise<string>; $getBlockHeader(hash: string): Promise<string>;

View File

@ -64,6 +64,13 @@ class BitcoinApi implements AbstractBitcoinApi {
}); });
} }
$getBlockHashTip(): Promise<string> {
return this.bitcoindClient.getChainTips()
.then((result: IBitcoinApi.ChainTips[]) => {
return result.find(tip => tip.status === 'active')!.hash;
});
}
$getTxIdsForBlock(hash: string): Promise<string[]> { $getTxIdsForBlock(hash: string): Promise<string[]> {
return this.bitcoindClient.getBlock(hash, 1) return this.bitcoindClient.getBlock(hash, 1)
.then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx); .then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);

View File

@ -25,6 +25,11 @@ class ElectrsApi implements AbstractBitcoinApi {
.then((response) => response.data); .then((response) => response.data);
} }
$getBlockHashTip(): Promise<string> {
return axios.get<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash', this.axiosConfig)
.then((response) => response.data);
}
$getTxIdsForBlock(hash: string): Promise<string[]> { $getTxIdsForBlock(hash: string): Promise<string[]> {
return axios.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig) return axios.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig)
.then((response) => response.data); .then((response) => response.data);

View File

@ -19,6 +19,9 @@ import HashratesRepository from '../repositories/HashratesRepository';
import indexer from '../indexer'; import indexer from '../indexer';
import poolsParser from './pools-parser'; import poolsParser from './pools-parser';
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository'; import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
import mining from './mining';
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
import difficultyAdjustment from './difficulty-adjustment';
class Blocks { class Blocks {
private blocks: BlockExtended[] = []; private blocks: BlockExtended[] = [];
@ -292,7 +295,8 @@ class Blocks {
} }
logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`); logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`);
} catch (e) { } catch (e) {
logger.err(`Blocks summaries indexing failed. Reason: ${(e instanceof Error ? e.message : e)}`); logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
throw e;
} }
} }
@ -300,12 +304,8 @@ class Blocks {
* [INDEXING] Index all blocks metadata for the mining dashboard * [INDEXING] Index all blocks metadata for the mining dashboard
*/ */
public async $generateBlockDatabase(): Promise<boolean> { public async $generateBlockDatabase(): Promise<boolean> {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
if (blockchainInfo.blocks !== blockchainInfo.headers) { // Wait for node to sync
return false;
}
try { try {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
let currentBlockHeight = blockchainInfo.blocks; let currentBlockHeight = blockchainInfo.blocks;
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, blockchainInfo.blocks); let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, blockchainInfo.blocks);
@ -368,18 +368,12 @@ class Blocks {
logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`); logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`);
loadingIndicators.setProgress('block-indexing', 100); loadingIndicators.setProgress('block-indexing', 100);
} catch (e) { } catch (e) {
logger.err('Block indexing failed. Trying again later. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e));
loadingIndicators.setProgress('block-indexing', 100); loadingIndicators.setProgress('block-indexing', 100);
return false; throw e;
} }
const chainValid = await BlocksRepository.$validateChain(); return await BlocksRepository.$validateChain();
if (!chainValid) {
indexer.reindex();
return false;
}
return true;
} }
public async $updateBlocks() { public async $updateBlocks() {
@ -449,7 +443,10 @@ class Blocks {
const newBlock = await this.$indexBlock(lastBlock['height'] - i); const newBlock = await this.$indexBlock(lastBlock['height'] - i);
await this.$getStrippedBlockTransactions(newBlock.id, true, true); await this.$getStrippedBlockTransactions(newBlock.id, true, true);
} }
logger.info(`Re-indexed 10 blocks and summaries`); await mining.$indexDifficultyAdjustments();
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`);
indexer.reindex();
} }
await blocksRepository.$saveBlockInDatabase(blockExtended); await blocksRepository.$saveBlockInDatabase(blockExtended);
@ -461,6 +458,15 @@ class Blocks {
} }
if (block.height % 2016 === 0) { if (block.height % 2016 === 0) {
if (Common.indexingEnabled()) {
await DifficultyAdjustmentsRepository.$saveAdjustments({
time: block.timestamp,
height: block.height,
difficulty: block.difficulty,
adjustment: Math.round((block.difficulty / this.currentDifficulty) * 1000000) / 1000000, // Remove float point noise
});
}
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100; this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
this.lastDifficultyAdjustmentTime = block.timestamp; this.lastDifficultyAdjustmentTime = block.timestamp;
this.currentDifficulty = block.difficulty; this.currentDifficulty = block.difficulty;

View File

@ -4,7 +4,7 @@ import logger from '../logger';
import { Common } from './common'; import { Common } from './common';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 21; private static currentVersion = 24;
private queryTimeout = 120000; private queryTimeout = 120000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@ -226,6 +226,28 @@ class DatabaseMigration {
await this.$executeQuery('DROP TABLE IF EXISTS `rates`'); await this.$executeQuery('DROP TABLE IF EXISTS `rates`');
await this.$executeQuery(this.getCreatePricesTableQuery(), await this.$checkIfTableExists('prices')); await this.$executeQuery(this.getCreatePricesTableQuery(), await this.$checkIfTableExists('prices'));
} }
if (databaseSchemaVersion < 22 && isBitcoin === true) {
await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`');
await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments'));
}
if (databaseSchemaVersion < 23) {
await this.$executeQuery('TRUNCATE `prices`');
await this.$executeQuery('ALTER TABLE `prices` DROP `avg_prices`');
await this.$executeQuery('ALTER TABLE `prices` ADD `USD` float DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `prices` ADD `EUR` float DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `prices` ADD `GBP` float DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `prices` ADD `CAD` float DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `prices` ADD `CHF` float DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `prices` ADD `AUD` float DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `prices` ADD `JPY` float DEFAULT "0"');
}
if (databaseSchemaVersion < 24 && isBitcoin == true) {
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
}
} catch (e) { } catch (e) {
throw e; throw e;
} }
@ -513,7 +535,7 @@ class DatabaseMigration {
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
} }
private getCreateRatesTableQuery(): string { private getCreateRatesTableQuery(): string { // This table has been replaced by the prices table
return `CREATE TABLE IF NOT EXISTS rates ( return `CREATE TABLE IF NOT EXISTS rates (
height int(10) unsigned NOT NULL, height int(10) unsigned NOT NULL,
bisq_rates JSON NOT NULL, bisq_rates JSON NOT NULL,
@ -539,6 +561,30 @@ class DatabaseMigration {
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
} }
private getCreateDifficultyAdjustmentsTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS difficulty_adjustments (
time timestamp NOT NULL,
height int(10) unsigned NOT NULL,
difficulty double unsigned NOT NULL,
adjustment float NOT NULL,
PRIMARY KEY (height),
INDEX (time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
private getCreateBlocksAuditsTableQuery(): string {
return `CREATE TABLE IF NOT EXISTS blocks_audits (
time timestamp NOT NULL,
hash varchar(65) NOT NULL,
height int(10) unsigned NOT NULL,
missing_txs JSON NOT NULL,
added_txs JSON NOT NULL,
match_rate float unsigned NOT NULL,
PRIMARY KEY (hash),
INDEX (height)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
}
public async $truncateIndexedData(tables: string[]) { public async $truncateIndexedData(tables: string[]) {
const allowedTables = ['blocks', 'hashrates', 'prices']; const allowedTables = ['blocks', 'hashrates', 'prices'];

View File

@ -1,4 +1,4 @@
import { PoolInfo, PoolStats, RewardStats } from '../mempool.interfaces'; import { IndexedDifficultyAdjustment, PoolInfo, PoolStats, RewardStats } from '../mempool.interfaces';
import BlocksRepository from '../repositories/BlocksRepository'; import BlocksRepository from '../repositories/BlocksRepository';
import PoolsRepository from '../repositories/PoolsRepository'; import PoolsRepository from '../repositories/PoolsRepository';
import HashratesRepository from '../repositories/HashratesRepository'; import HashratesRepository from '../repositories/HashratesRepository';
@ -7,11 +7,25 @@ import logger from '../logger';
import { Common } from './common'; import { Common } from './common';
import loadingIndicators from './loading-indicators'; import loadingIndicators from './loading-indicators';
import { escape } from 'mysql2'; import { escape } from 'mysql2';
import indexer from '../indexer';
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
import config from '../config';
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
class Mining { class Mining {
constructor() { constructor() {
} }
/**
* Get historical block predictions match rate
*/
public async $getBlockPredictionsHistory(interval: string | null = null): Promise<any> {
return await BlocksAuditsRepository.$getBlockPredictionsHistory(
this.getTimeRange(interval),
Common.getSqlInterval(interval)
);
}
/** /**
* Get historical block total fee * Get historical block total fee
*/ */
@ -262,6 +276,7 @@ class Mining {
loadingIndicators.setProgress('weekly-hashrate-indexing', 100); loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('weekly-hashrate-indexing', 100); loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
logger.err(`Weekly mining pools hashrates indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
throw e; throw e;
} }
} }
@ -301,7 +316,7 @@ class Mining {
while (toTimestamp > genesisTimestamp) { while (toTimestamp > genesisTimestamp) {
const fromTimestamp = toTimestamp - 86400000; const fromTimestamp = toTimestamp - 86400000;
// Skip already indexed weeks // Skip already indexed days
if (indexedTimestamp.includes(toTimestamp / 1000)) { if (indexedTimestamp.includes(toTimestamp / 1000)) {
toTimestamp -= 86400000; toTimestamp -= 86400000;
++totalIndexed; ++totalIndexed;
@ -312,7 +327,7 @@ class Mining {
// we are currently indexing has complete data) // we are currently indexing has complete data)
const blockStatsPreviousDay: any = await BlocksRepository.$blockCountBetweenTimestamp( const blockStatsPreviousDay: any = await BlocksRepository.$blockCountBetweenTimestamp(
null, (fromTimestamp - 86400000) / 1000, (toTimestamp - 86400000) / 1000); null, (fromTimestamp - 86400000) / 1000, (toTimestamp - 86400000) / 1000);
if (blockStatsPreviousDay.blockCount === 0) { // We are done indexing if (blockStatsPreviousDay.blockCount === 0 && config.MEMPOOL.NETWORK === 'mainnet') { // We are done indexing
break; break;
} }
@ -356,9 +371,10 @@ class Mining {
// Add genesis block manually // Add genesis block manually
if (toTimestamp <= genesisTimestamp && !indexedTimestamp.includes(genesisTimestamp)) { if (toTimestamp <= genesisTimestamp && !indexedTimestamp.includes(genesisTimestamp)) {
hashrates.push({ hashrates.push({
hashrateTimestamp: genesisTimestamp, hashrateTimestamp: genesisTimestamp / 1000,
avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1), avgHashrate: await bitcoinClient.getNetworkHashPs(1, 1),
poolId: null, poolId: 0,
share: 1,
type: 'daily', type: 'daily',
}); });
} }
@ -373,10 +389,62 @@ class Mining {
loadingIndicators.setProgress('daily-hashrate-indexing', 100); loadingIndicators.setProgress('daily-hashrate-indexing', 100);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('daily-hashrate-indexing', 100); loadingIndicators.setProgress('daily-hashrate-indexing', 100);
logger.err(`Daily network hashrate indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
throw e; throw e;
} }
} }
/**
* Index difficulty adjustments
*/
public async $indexDifficultyAdjustments(): Promise<void> {
const indexedHeightsArray = await DifficultyAdjustmentsRepository.$getAdjustmentsHeights();
const indexedHeights = {};
for (const height of indexedHeightsArray) {
indexedHeights[height] = true;
}
const blocks: any = await BlocksRepository.$getBlocksDifficulty();
let currentDifficulty = 0;
let totalIndexed = 0;
if (indexedHeights[0] === false) {
await DifficultyAdjustmentsRepository.$saveAdjustments({
time: 1231006505,
height: 0,
difficulty: 1.0,
adjustment: 0.0,
});
}
for (const block of blocks) {
if (block.difficulty !== currentDifficulty) {
if (block.height === 0 || indexedHeights[block.height] === true) { // Already indexed
currentDifficulty = block.difficulty;
continue;
}
let adjustment = block.difficulty / Math.max(1, currentDifficulty);
adjustment = Math.round(adjustment * 1000000) / 1000000; // Remove float point noise
await DifficultyAdjustmentsRepository.$saveAdjustments({
time: block.time,
height: block.height,
difficulty: block.difficulty,
adjustment: adjustment,
});
totalIndexed++;
currentDifficulty = block.difficulty;
}
}
if (totalIndexed > 0) {
logger.notice(`Indexed ${totalIndexed} difficulty adjustments`);
}
}
private getDateMidnight(date: Date): Date { private getDateMidnight(date: Date): Date {
date.setUTCHours(0); date.setUTCHours(0);
date.setUTCMinutes(0); date.setUTCMinutes(0);

View File

@ -16,6 +16,7 @@ import transactionUtils from './transaction-utils';
import rbfCache from './rbf-cache'; import rbfCache from './rbf-cache';
import difficultyAdjustment from './difficulty-adjustment'; import difficultyAdjustment from './difficulty-adjustment';
import feeApi from './fee-api'; import feeApi from './fee-api';
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
class WebsocketHandler { class WebsocketHandler {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@ -416,17 +417,40 @@ class WebsocketHandler {
if (_mempoolBlocks[0]) { if (_mempoolBlocks[0]) {
const matches: string[] = []; const matches: string[] = [];
const added: string[] = [];
const missing: string[] = [];
for (const txId of txIds) { for (const txId of txIds) {
if (_mempoolBlocks[0].transactionIds.indexOf(txId) > -1) { if (_mempoolBlocks[0].transactionIds.indexOf(txId) > -1) {
matches.push(txId); matches.push(txId);
} else {
added.push(txId);
} }
delete _memPool[txId]; delete _memPool[txId];
} }
matchRate = Math.round((matches.length / (txIds.length - 1)) * 100); for (const txId of _mempoolBlocks[0].transactionIds) {
if (matches.includes(txId) || added.includes(txId)) {
continue;
}
missing.push(txId);
}
matchRate = Math.round((Math.max(0, matches.length - missing.length - added.length) / txIds.length * 100) * 100) / 100;
mempoolBlocks.updateMempoolBlocks(_memPool); mempoolBlocks.updateMempoolBlocks(_memPool);
mBlocks = mempoolBlocks.getMempoolBlocks(); mBlocks = mempoolBlocks.getMempoolBlocks();
mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas(); mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
if (Common.indexingEnabled()) {
BlocksAuditsRepository.$saveAudit({
time: block.timestamp,
height: block.height,
hash: block.id,
addedTxs: added,
missingTxs: missing,
matchRate: matchRate,
});
}
} }
if (block.extras) { if (block.extras) {

View File

@ -28,6 +28,7 @@ import { Common } from './api/common';
import poolsUpdater from './tasks/pools-updater'; import poolsUpdater from './tasks/pools-updater';
import indexer from './indexer'; import indexer from './indexer';
import priceUpdater from './tasks/price-updater'; import priceUpdater from './tasks/price-updater';
import BlocksAuditsRepository from './repositories/BlocksAuditsRepository';
class Server { class Server {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@ -285,11 +286,14 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug', routes.$getPool) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/pool/:slug', routes.$getPool)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', routes.$getPoolsHistoricalHashrate) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/pools/:interval', routes.$getPoolsHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/hashrate/:interval', routes.$getHistoricalHashrate)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments', routes.$getDifficultyAdjustments)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', routes.$getRewardStats) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', routes.$getRewardStats)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', routes.$getHistoricalBlockFees) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', routes.$getHistoricalBlockFees)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', routes.$getHistoricalBlockRewards) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', routes.$getHistoricalBlockRewards)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', routes.$getHistoricalBlockFeeRates) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', routes.$getHistoricalBlockFeeRates)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', routes.$getHistoricalBlockSizeAndWeight) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', routes.$getHistoricalBlockSizeAndWeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments/:interval', routes.$getDifficultyAdjustments)
.get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/predictions/:interval', routes.$getHistoricalBlockPrediction)
; ;
} }
@ -332,6 +336,7 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', routes.getBlockHeader) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', routes.getBlockHeader)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', routes.getBlockTipHeight) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', routes.getBlockTipHeight)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', routes.getBlockTipHash)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', routes.getBlockTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', routes.getBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', routes.getBlockTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', routes.getBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', routes.getTxIdsForBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', routes.getTxIdsForBlock)

View File

@ -4,6 +4,7 @@ import mempool from './api/mempool';
import mining from './api/mining'; import mining from './api/mining';
import logger from './logger'; import logger from './logger';
import HashratesRepository from './repositories/HashratesRepository'; import HashratesRepository from './repositories/HashratesRepository';
import bitcoinClient from './api/bitcoin/bitcoin-client';
class Indexer { class Indexer {
runIndexer = true; runIndexer = true;
@ -25,6 +26,12 @@ class Indexer {
return; return;
} }
// Do not attempt to index anything unless Bitcoin Core is fully synced
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
if (blockchainInfo.blocks !== blockchainInfo.headers) {
return;
}
this.runIndexer = false; this.runIndexer = false;
this.indexerRunning = true; this.indexerRunning = true;
@ -32,17 +39,21 @@ class Indexer {
const chainValid = await blocks.$generateBlockDatabase(); const chainValid = await blocks.$generateBlockDatabase();
if (chainValid === false) { if (chainValid === false) {
// Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration // Chain of block hash was invalid, so we need to reindex. Stop here and continue at the next iteration
logger.warn(`The chain of block hash is invalid, re-indexing invalid data in 10 seconds.`);
setTimeout(() => this.reindex(), 10000);
this.indexerRunning = false; this.indexerRunning = false;
return; return;
} }
await mining.$indexDifficultyAdjustments();
await this.$resetHashratesIndexingState(); await this.$resetHashratesIndexingState();
await mining.$generateNetworkHashrateHistory(); await mining.$generateNetworkHashrateHistory();
await mining.$generatePoolHashrateHistory(); await mining.$generatePoolHashrateHistory();
await blocks.$generateBlocksSummariesDatabase(); await blocks.$generateBlocksSummariesDatabase();
} catch (e) { } catch (e) {
this.reindex(); this.indexerRunning = false;
logger.err(`Indexer failed, trying again later. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Indexer failed, trying again in 10 seconds. Reason: ` + (e instanceof Error ? e.message : e));
setTimeout(() => this.reindex(), 10000);
} }
this.indexerRunning = false; this.indexerRunning = false;
@ -54,6 +65,7 @@ class Indexer {
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', 0); await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', 0);
} catch (e) { } catch (e) {
logger.err(`Cannot reset hashrate indexing timestamps. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot reset hashrate indexing timestamps. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
} }
} }
} }

View File

@ -22,6 +22,15 @@ export interface PoolStats extends PoolInfo {
emptyBlocks: number; emptyBlocks: number;
} }
export interface BlockAudit {
time: number,
height: number,
hash: string,
missingTxs: string[],
addedTxs: string[],
matchRate: number,
}
export interface MempoolBlock { export interface MempoolBlock {
blockSize: number; blockSize: number;
blockVSize: number; blockVSize: number;
@ -224,6 +233,13 @@ export interface IDifficultyAdjustment {
timeOffset: number; timeOffset: number;
} }
export interface IndexedDifficultyAdjustment {
time: number; // UNIX timestamp
height: number; // Block height
difficulty: number;
adjustment: number;
}
export interface RewardStats { export interface RewardStats {
totalReward: number; totalReward: number;
totalFee: number; totalFee: number;

View File

@ -0,0 +1,51 @@
import DB from '../database';
import logger from '../logger';
import { BlockAudit } from '../mempool.interfaces';
class BlocksAuditRepositories {
public async $saveAudit(audit: BlockAudit): Promise<void> {
try {
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, match_rate)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
JSON.stringify(audit.addedTxs), audit.matchRate]);
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
} else {
logger.err(`Cannot save block audit into db. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
}
public async $getBlockPredictionsHistory(div: number, interval: string | null): Promise<any> {
try {
let query = `SELECT UNIX_TIMESTAMP(time) as time, height, match_rate FROM blocks_audits`;
if (interval !== null) {
query += ` WHERE time BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += ` GROUP BY UNIX_TIMESTAMP(time) DIV ${div} ORDER BY height`;
const [rows] = await DB.query(query);
return rows;
} catch (e: any) {
logger.err(`Cannot fetch block prediction history. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getPredictionsCount(): Promise<number> {
try {
const [rows] = await DB.query(`SELECT count(hash) as count FROM blocks_audits`);
return rows[0].count;
} catch (e: any) {
logger.err(`Cannot fetch block prediction history. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
}
export default new BlocksAuditRepositories();

View File

@ -7,6 +7,7 @@ import PoolsRepository from './PoolsRepository';
import HashratesRepository from './HashratesRepository'; import HashratesRepository from './HashratesRepository';
import { escape } from 'mysql2'; import { escape } from 'mysql2';
import BlocksSummariesRepository from './BlocksSummariesRepository'; import BlocksSummariesRepository from './BlocksSummariesRepository';
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
class BlocksRepository { class BlocksRepository {
/** /**
@ -381,48 +382,9 @@ class BlocksRepository {
/** /**
* Return blocks difficulty * Return blocks difficulty
*/ */
public async $getBlocksDifficulty(interval: string | null): Promise<object[]> { public async $getBlocksDifficulty(): Promise<object[]> {
interval = Common.getSqlInterval(interval);
// :D ... Yeah don't ask me about this one https://stackoverflow.com/a/40303162
// Basically, using temporary user defined fields, we are able to extract all
// difficulty adjustments from the blocks tables.
// This allow use to avoid indexing it in another table.
let query = `
SELECT
*
FROM
(
SELECT
UNIX_TIMESTAMP(blockTimestamp) as timestamp, difficulty, height,
IF(@prevStatus = YT.difficulty, @rn := @rn + 1,
IF(@prevStatus := YT.difficulty, @rn := 1, @rn := 1)
) AS rn
FROM blocks YT
CROSS JOIN
(
SELECT @prevStatus := -1, @rn := 1
) AS var
`;
if (interval) {
query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += `
ORDER BY YT.height
) AS t
WHERE t.rn = 1
ORDER BY t.height
`;
try { try {
const [rows]: any[] = await DB.query(query); const [rows]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(blockTimestamp) as time, height, difficulty FROM blocks`);
for (const row of rows) {
delete row['rn'];
}
return rows; return rows;
} catch (e) { } catch (e) {
logger.err('Cannot generate difficulty history. Reason: ' + (e instanceof Error ? e.message : e)); logger.err('Cannot generate difficulty history. Reason: ' + (e instanceof Error ? e.message : e));
@ -452,26 +414,6 @@ class BlocksRepository {
} }
} }
/*
* Check if the last 10 blocks chain is valid
*/
public async $validateRecentBlocks(): Promise<boolean> {
try {
const [lastBlocks]: any[] = await DB.query(`SELECT height, hash, previous_block_hash FROM blocks ORDER BY height DESC LIMIT 10`);
for (let i = 0; i < lastBlocks.length - 1; ++i) {
if (lastBlocks[i].previous_block_hash !== lastBlocks[i + 1].hash) {
logger.warn(`Chain divergence detected at block ${lastBlocks[i].height}, re-indexing most recent data`);
return false;
}
}
return true;
} catch (e) {
return true; // Don't do anything if there is a db error
}
}
/** /**
* Check if the chain of block hash is valid and delete data from the stale branch if needed * Check if the chain of block hash is valid and delete data from the stale branch if needed
*/ */
@ -494,10 +436,11 @@ class BlocksRepository {
} }
if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) { if (blocks[idx].previous_block_hash !== blocks[idx - 1].hash) {
logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}, re-indexing newer blocks and hashrates`); logger.warn(`Chain divergence detected at block ${blocks[idx - 1].height}`);
await this.$deleteBlocksFrom(blocks[idx - 1].height); await this.$deleteBlocksFrom(blocks[idx - 1].height);
await BlocksSummariesRepository.$deleteBlocksFrom(blocks[idx - 1].height); await BlocksSummariesRepository.$deleteBlocksFrom(blocks[idx - 1].height);
await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800); await HashratesRepository.$deleteHashratesFromTimestamp(blocks[idx - 1].timestamp - 604800);
await DifficultyAdjustmentsRepository.$deleteAdjustementsFromHeight(blocks[idx - 1].height);
return false; return false;
} }
++idx; ++idx;

View File

@ -0,0 +1,95 @@
import { Common } from '../api/common';
import config from '../config';
import DB from '../database';
import logger from '../logger';
import { IndexedDifficultyAdjustment } from '../mempool.interfaces';
class DifficultyAdjustmentsRepository {
public async $saveAdjustments(adjustment: IndexedDifficultyAdjustment): Promise<void> {
if (adjustment.height === 1) {
return;
}
try {
const query = `INSERT INTO difficulty_adjustments(time, height, difficulty, adjustment) VALUE (FROM_UNIXTIME(?), ?, ?, ?)`;
const params: any[] = [
adjustment.time,
adjustment.height,
adjustment.difficulty,
adjustment.adjustment,
];
await DB.query(query, params);
} catch (e: any) {
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
logger.debug(`Cannot save difficulty adjustment at block ${adjustment.height}, already indexed, ignoring`);
} else {
logger.err(`Cannot save difficulty adjustment at block ${adjustment.height}. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
}
public async $getAdjustments(interval: string | null, descOrder: boolean = false): Promise<IndexedDifficultyAdjustment[]> {
interval = Common.getSqlInterval(interval);
let query = `SELECT
CAST(AVG(UNIX_TIMESTAMP(time)) as INT) as time,
CAST(AVG(height) AS INT) as height,
CAST(AVG(difficulty) as DOUBLE) as difficulty,
CAST(AVG(adjustment) as DOUBLE) as adjustment
FROM difficulty_adjustments`;
if (interval) {
query += ` WHERE time BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
}
query += ` GROUP BY UNIX_TIMESTAMP(time) DIV ${86400}`;
if (descOrder === true) {
query += ` ORDER BY time DESC`;
} else {
query += ` ORDER BY time`;
}
try {
const [rows] = await DB.query(query);
return rows as IndexedDifficultyAdjustment[];
} catch (e) {
logger.err(`Cannot get difficulty adjustments from the database. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
public async $getAdjustmentsHeights(): Promise<number[]> {
try {
const [rows]: any[] = await DB.query(`SELECT height FROM difficulty_adjustments`);
return rows.map(block => block.height);
} catch (e: any) {
logger.err(`Cannot get difficulty adjustment block heights. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
public async $deleteAdjustementsFromHeight(height: number): Promise<void> {
try {
logger.info(`Delete newer difficulty adjustments from height ${height} from the database`);
await DB.query(`DELETE FROM difficulty_adjustments WHERE height >= ?`, [height]);
} catch (e: any) {
logger.err(`Cannot delete difficulty adjustments from the database. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
public async $deleteLastAdjustment(): Promise<void> {
try {
logger.info(`Delete last difficulty adjustment from the database`);
await DB.query(`DELETE FROM difficulty_adjustments ORDER BY time LIMIT 1`);
} catch (e: any) {
logger.err(`Cannot delete last difficulty adjustment from the database. Reason: ${e instanceof Error ? e.message : e}`);
throw e;
}
}
}
export default new DifficultyAdjustmentsRepository();

View File

@ -1,5 +1,6 @@
import { escape } from 'mysql2'; import { escape } from 'mysql2';
import { Common } from '../api/common'; import { Common } from '../api/common';
import config from '../config';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
import PoolsRepository from './PoolsRepository'; import PoolsRepository from './PoolsRepository';
@ -32,7 +33,9 @@ class HashratesRepository {
public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> { public async $getNetworkDailyHashrate(interval: string | null): Promise<any[]> {
interval = Common.getSqlInterval(interval); interval = Common.getSqlInterval(interval);
let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate let query = `SELECT
CAST(AVG(UNIX_TIMESTAMP(hashrate_timestamp)) as INT) as timestamp,
CAST(AVG(avg_hashrate) as DOUBLE) as avgHashrate
FROM hashrates`; FROM hashrates`;
if (interval) { if (interval) {
@ -42,6 +45,7 @@ class HashratesRepository {
query += ` WHERE hashrates.type = 'daily'`; query += ` WHERE hashrates.type = 'daily'`;
} }
query += ` GROUP BY UNIX_TIMESTAMP(hashrate_timestamp) DIV ${86400}`;
query += ` ORDER by hashrate_timestamp`; query += ` ORDER by hashrate_timestamp`;
try { try {
@ -75,6 +79,9 @@ class HashratesRepository {
interval = Common.getSqlInterval(interval); interval = Common.getSqlInterval(interval);
const topPoolsId = (await PoolsRepository.$getPoolsInfo('1w')).map((pool) => pool.poolId); const topPoolsId = (await PoolsRepository.$getPoolsInfo('1w')).map((pool) => pool.poolId);
if (topPoolsId.length === 0) {
return [];
}
let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate, share, pools.name as poolName let query = `SELECT UNIX_TIMESTAMP(hashrate_timestamp) as timestamp, avg_hashrate as avgHashrate, share, pools.name as poolName
FROM hashrates FROM hashrates

View File

@ -5,7 +5,11 @@ import { Prices } from '../tasks/price-updater';
class PricesRepository { class PricesRepository {
public async $savePrices(time: number, prices: Prices): Promise<void> { public async $savePrices(time: number, prices: Prices): Promise<void> {
try { try {
await DB.query(`INSERT INTO prices(time, avg_prices) VALUE (FROM_UNIXTIME(?), ?)`, [time, JSON.stringify(prices)]); await DB.query(`
INSERT INTO prices(time, USD, EUR, GBP, CAD, CHF, AUD, JPY)
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ? )`,
[time, prices.USD, prices.EUR, prices.GBP, prices.CAD, prices.CHF, prices.AUD, prices.JPY]
);
} catch (e: any) { } catch (e: any) {
logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e)); logger.err(`Cannot save exchange rate into db. Reason: ` + (e instanceof Error ? e.message : e));
throw e; throw e;

View File

@ -26,6 +26,8 @@ import mining from './api/mining';
import BlocksRepository from './repositories/BlocksRepository'; import BlocksRepository from './repositories/BlocksRepository';
import HashratesRepository from './repositories/HashratesRepository'; import HashratesRepository from './repositories/HashratesRepository';
import difficultyAdjustment from './api/difficulty-adjustment'; import difficultyAdjustment from './api/difficulty-adjustment';
import DifficultyAdjustmentsRepository from './repositories/DifficultyAdjustmentsRepository';
import BlocksAuditsRepository from './repositories/BlocksAuditsRepository';
class Routes { class Routes {
constructor() {} constructor() {}
@ -653,7 +655,7 @@ class Routes {
try { try {
const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval); const hashrates = await HashratesRepository.$getNetworkDailyHashrate(req.params.interval);
const difficulty = await BlocksRepository.$getBlocksDifficulty(req.params.interval); const difficulty = await DifficultyAdjustmentsRepository.$getAdjustments(req.params.interval, false);
const blockCount = await BlocksRepository.$blockCount(null, null); const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
@ -730,6 +732,32 @@ class Routes {
} }
} }
public async $getDifficultyAdjustments(req: Request, res: Response) {
try {
const difficulty = await DifficultyAdjustmentsRepository.$getAdjustments(req.params.interval, true);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async $getHistoricalBlockPrediction(req: Request, res: Response) {
try {
const blockPredictions = await mining.$getBlockPredictionsHistory(req.params.interval);
const blockCount = await BlocksAuditsRepository.$getPredictionsCount();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockPredictions.map(prediction => [prediction.time, prediction.height, prediction.match_rate]));
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async getBlock(req: Request, res: Response) { public async getBlock(req: Request, res: Response) {
try { try {
const block = await blocks.$getBlock(req.params.hash); const block = await blocks.$getBlock(req.params.hash);
@ -925,6 +953,16 @@ class Routes {
} }
} }
public async getBlockTipHash(req: Request, res: Response) {
try {
const result = await bitcoinApi.$getBlockHashTip();
res.setHeader('content-type', 'text/plain');
res.send(result);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
public async getTxIdsForBlock(req: Request, res: Response) { public async getTxIdsForBlock(req: Request, res: Response) {
try { try {
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);

View File

@ -87,7 +87,7 @@ class KrakenApi implements PriceFeed {
} }
if (Object.keys(priceHistory).length > 0) { if (Object.keys(priceHistory).length > 0) {
logger.info(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`); logger.notice(`Inserted ${Object.keys(priceHistory).length} Kraken EUR, USD, GBP, JPY, CAD, CHF and AUD weekly price history into db`);
} }
} }
} }

View File

@ -176,7 +176,7 @@ class PriceUpdater {
++insertedCount; ++insertedCount;
} }
if (insertedCount > 0) { if (insertedCount > 0) {
logger.info(`Inserted ${insertedCount} MtGox USD weekly price history into db`); logger.notice(`Inserted ${insertedCount} MtGox USD weekly price history into db`);
} }
// Insert Kraken weekly prices // Insert Kraken weekly prices
@ -205,23 +205,23 @@ class PriceUpdater {
try { try {
historicalPrices.push(await feed.$fetchRecentHourlyPrice(this.currencies)); historicalPrices.push(await feed.$fetchRecentHourlyPrice(this.currencies));
} catch (e) { } catch (e) {
logger.info(`Cannot fetch hourly historical price from ${feed.name}. Ignoring this feed. Reason: ${e instanceof Error ? e.message : e}`); logger.err(`Cannot fetch hourly historical price from ${feed.name}. Ignoring this feed. Reason: ${e instanceof Error ? e.message : e}`);
} }
} }
// Group them by timestamp and currency, for example // Group them by timestamp and currency, for example
// grouped[123456789]['USD'] = [1, 2, 3, 4]; // grouped[123456789]['USD'] = [1, 2, 3, 4];
let grouped: Object = {}; const grouped: Object = {};
for (const historicalEntry of historicalPrices) { for (const historicalEntry of historicalPrices) {
for (const time in historicalEntry) { for (const time in historicalEntry) {
if (existingPriceTimes.includes(parseInt(time, 10))) { if (existingPriceTimes.includes(parseInt(time, 10))) {
continue; continue;
} }
if (grouped[time] == undefined) { if (grouped[time] === undefined) {
grouped[time] = { grouped[time] = {
USD: [], EUR: [], GBP: [], CAD: [], CHF: [], AUD: [], JPY: [] USD: [], EUR: [], GBP: [], CAD: [], CHF: [], AUD: [], JPY: []
} };
} }
for (const currency of this.currencies) { for (const currency of this.currencies) {
@ -238,13 +238,20 @@ class PriceUpdater {
for (const time in grouped) { for (const time in grouped) {
const prices: Prices = this.getEmptyPricesObj(); const prices: Prices = this.getEmptyPricesObj();
for (const currency in grouped[time]) { for (const currency in grouped[time]) {
prices[currency] = Math.round((grouped[time][currency].reduce((partialSum, a) => partialSum + a, 0)) / grouped[time][currency].length); if (grouped[time][currency].length === 0) {
continue;
}
prices[currency] = Math.round((grouped[time][currency].reduce(
(partialSum, a) => partialSum + a, 0)
) / grouped[time][currency].length);
} }
await PricesRepository.$savePrices(parseInt(time, 10), prices); await PricesRepository.$savePrices(parseInt(time, 10), prices);
++totalInserted; ++totalInserted;
} }
logger.info(`Inserted ${totalInserted} hourly historical prices into the db`); if (totalInserted > 0) {
logger.notice(`Inserted ${totalInserted} hourly historical prices into the db`);
}
} }
} }

View File

@ -50,10 +50,14 @@ export async function query(path): Promise<object | undefined> {
} }
return data.data; return data.data;
} catch (e) { } catch (e) {
logger.err(`Could not connect to ${path}. Reason: ` + (e instanceof Error ? e.message : e)); logger.warn(`Could not connect to ${path} (Attempt ${retry + 1}/${config.MEMPOOL.EXTERNAL_MAX_RETRY}). Reason: ` + (e instanceof Error ? e.message : e));
retry++; retry++;
} }
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL); if (retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
}
} }
logger.err(`Could not connect to ${path}. All ${config.MEMPOOL.EXTERNAL_MAX_RETRY} attempts failed`);
return undefined; return undefined;
} }

3
frontend/.eslintignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
frontend

34
frontend/.eslintrc Normal file
View File

@ -0,0 +1,34 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/ban-ts-comment": 1,
"@typescript-eslint/ban-types": 1,
"@typescript-eslint/no-empty-function": 1,
"@typescript-eslint/no-explicit-any": 1,
"@typescript-eslint/no-inferrable-types": 1,
"@typescript-eslint/no-namespace": 1,
"@typescript-eslint/no-this-alias": 1,
"@typescript-eslint/no-var-requires": 1,
"no-case-declarations": 1,
"no-console": 1,
"no-constant-condition": 1,
"no-dupe-else-if": 1,
"no-empty": 1,
"no-extra-boolean-cast": 1,
"no-prototype-builtins": 1,
"no-self-assign": 1,
"no-useless-catch": 1,
"no-var": 1,
"prefer-const": 1,
"prefer-rest-params": 1
}
}

View File

@ -248,23 +248,6 @@
"browserTarget": "mempool:build" "browserTarget": "mempool:build"
} }
}, },
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/resources"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
},
"e2e": { "e2e": {
"builder": "@cypress/schematic:cypress", "builder": "@cypress/schematic:cypress",
"options": { "options": {

View File

@ -1,32 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/mempool'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

File diff suppressed because it is too large Load Diff

View File

@ -42,7 +42,8 @@
"build-mempool-bisq-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-bisq.js --standalone bisqJS > ./dist/mempool/browser/en-US/bisq.js", "build-mempool-bisq-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-bisq.js --standalone bisqJS > ./dist/mempool/browser/en-US/bisq.js",
"build-mempool-liquid-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-liquid.js --standalone liquidJS > ./dist/mempool/browser/en-US/liquid.js", "build-mempool-liquid-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-liquid.js --standalone liquidJS > ./dist/mempool/browser/en-US/liquid.js",
"test": "npm run ng -- test", "test": "npm run ng -- test",
"lint": "npm run ng -- lint", "lint": "./node_modules/.bin/eslint . --ext .ts",
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
"e2e": "npm run generate-config && npm run ng -- e2e", "e2e": "npm run generate-config && npm run ng -- e2e",
"e2e:ci": "npm run cypress:run:ci", "e2e:ci": "npm run cypress:run:ci",
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config", "config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
@ -77,7 +78,6 @@
"@fortawesome/fontawesome-common-types": "~6.1.1", "@fortawesome/fontawesome-common-types": "~6.1.1",
"@fortawesome/fontawesome-svg-core": "~6.1.1", "@fortawesome/fontawesome-svg-core": "~6.1.1",
"@fortawesome/free-solid-svg-icons": "~6.1.1", "@fortawesome/free-solid-svg-icons": "~6.1.1",
"@juggle/resize-observer": "^3.3.1",
"@mempool/mempool.js": "2.3.0", "@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^11.0.0", "@ng-bootstrap/ng-bootstrap": "^11.0.0",
"@nguniversal/express-engine": "~13.1.1", "@nguniversal/express-engine": "~13.1.1",
@ -88,7 +88,7 @@
"domino": "^2.1.6", "domino": "^2.1.6",
"echarts": "~5.3.2", "echarts": "~5.3.2",
"express": "^4.17.1", "express": "^4.17.1",
"lightweight-charts": "^3.3.0", "lightweight-charts": "~3.8.0",
"ngx-bootrap-multiselect": "^2.0.0", "ngx-bootrap-multiselect": "^2.0.0",
"ngx-echarts": "8.0.1", "ngx-echarts": "8.0.1",
"ngx-infinite-scroll": "^10.0.1", "ngx-infinite-scroll": "^10.0.1",
@ -104,28 +104,23 @@
"@angular/language-service": "~13.3.10", "@angular/language-service": "~13.3.10",
"@nguniversal/builders": "~13.1.1", "@nguniversal/builders": "~13.1.1",
"@types/express": "^4.17.0", "@types/express": "^4.17.0",
"@types/jasmine": "~4.0.3",
"@types/jasminewd2": "~2.0.10",
"@types/node": "^12.11.1", "@types/node": "^12.11.1",
"codelyzer": "~6.0.2", "@typescript-eslint/eslint-plugin": "^5.30.5",
"http-proxy-middleware": "^1.0.5", "@typescript-eslint/parser": "^5.30.5",
"jasmine-core": "~4.1.0", "eslint": "^8.19.0",
"jasmine-spec-reporter": "~7.0.0", "http-proxy-middleware": "~2.0.6",
"karma": "~6.3.19", "ts-node": "~10.8.1",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~5.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~4.6.4" "typescript": "~4.6.4"
}, },
"optionalDependencies": { "optionalDependencies": {
"@cypress/schematic": "^1.3.0", "@cypress/schematic": "~2.0.0",
"cypress": "^10.0.2", "cypress": "^10.0.2",
"cypress-fail-on-console-error": "^2.1.3", "cypress-fail-on-console-error": "~2.1.4",
"cypress-wait-until": "^1.7.1", "cypress-wait-until": "^1.7.1",
"mock-socket": "^9.0.3", "mock-socket": "~9.1.4",
"start-server-and-test": "^1.12.6" "start-server-and-test": "~1.14.0"
},
"scarfSettings": {
"enabled": false
} }
} }

View File

@ -171,7 +171,7 @@ let routes: Routes = [
{ {
path: 'block', path: 'block',
component: StartComponent, component: StartComponent,
children: [ children: [
{ {
path: ':id', path: ':id',
component: BlockComponent component: BlockComponent
@ -258,7 +258,7 @@ let routes: Routes = [
{ {
path: 'block', path: 'block',
component: StartComponent, component: StartComponent,
children: [ children: [
{ {
path: ':id', path: ':id',
component: BlockComponent component: BlockComponent
@ -361,7 +361,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{ {
path: 'block', path: 'block',
component: StartComponent, component: StartComponent,
children: [ children: [
{ {
path: ':id', path: ':id',
component: BlockComponent component: BlockComponent
@ -465,7 +465,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{ {
path: 'block', path: 'block',
component: StartComponent, component: StartComponent,
children: [ children: [
{ {
path: ':id', path: ':id',
component: BlockComponent component: BlockComponent

View File

@ -31,7 +31,7 @@ export class BisqTransfersComponent implements OnInit, OnChanges {
} }
ngOnChanges() { ngOnChanges() {
this.totalOutput = this.tx.outputs.filter((output) => output.isVerified).reduce((acc, output) => acc + output.bsqAmount, 0);; this.totalOutput = this.tx.outputs.filter((output) => output.isVerified).reduce((acc, output) => acc + output.bsqAmount, 0);
} }
switchCurrency() { switchCurrency() {

View File

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AddressLabelsComponent } from './address-labels.component';
describe('AddressLabelsComponent', () => {
let component: AddressLabelsComponent;
let fixture: ComponentFixture<AddressLabelsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AddressLabelsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddressLabelsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -130,7 +130,7 @@
<span i18n="address.error.loading-address-data">Error loading address data.</span> <span i18n="address.error.loading-address-data">Error loading address data.</span>
<br> <br>
<ng-template #displayServerError><i class="small">({{ error.error }})</i></ng-template> <ng-template #displayServerError><i class="small">({{ error.error }})</i></ng-template>
<ng-template [ngIf]="error.status === 413 || error.status === 405" [ngIfElse]="displayServerError"> <ng-template [ngIf]="error.status === 413 || error.status === 405 || error.status === 504" [ngIfElse]="displayServerError">
<ng-container i18n="Electrum server limit exceeded error"> <ng-container i18n="Electrum server limit exceeded error">
<i>There many transactions on this address, more than your backend can handle. See more on <a href="/docs/faq#address-lookup-issues">setting up a stronger backend</a>.</i> <i>There many transactions on this address, more than your backend can handle. See more on <a href="/docs/faq#address-lookup-issues">setting up a stronger backend</a>.</i>
<br><br> <br><br>

View File

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AmountComponent } from './amount.component';
describe('AmountComponent', () => {
let component: AmountComponent;
let fixture: ComponentFixture<AmountComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AmountComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AmountComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,35 +0,0 @@
import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'mempool'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('mempool');
});
it('should render title in a h1 tag', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to mempool!');
});
});

View File

@ -35,7 +35,7 @@
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680"> <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 3Y <input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680"> <label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> ALL <input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]"> ALL
</label> </label>
</div> </div>

View File

@ -79,57 +79,3 @@
} }
} }
} }
.pool-distribution {
min-height: 56px;
display: block;
@media (min-width: 485px) {
display: flex;
flex-direction: row;
}
h5 {
margin-bottom: 10px;
}
.item {
width: 50%;
display: inline-block;
margin: 0px auto 20px;
&:nth-child(2) {
order: 2;
@media (min-width: 485px) {
order: 3;
}
}
&:nth-child(3) {
order: 3;
@media (min-width: 485px) {
order: 2;
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;
}
.card-text {
font-size: 18px;
span {
color: #ffffff66;
font-size: 12px;
}
}
}
}
.skeleton-loader {
width: 100%;
display: block;
max-width: 80px;
margin: 15px auto 3px;
}

View File

@ -174,7 +174,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
align: 'left', align: 'left',
}, },
borderColor: '#000', borderColor: '#000',
formatter: function (data) { formatter: function(data) {
if (data.length <= 0) { if (data.length <= 0) {
return ''; return '';
} }

View File

@ -35,7 +35,7 @@
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680"> <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 3Y <input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680"> <label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> ALL <input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-fees' | relativeUrl]"> ALL
</label> </label>
</div> </div>

View File

@ -79,57 +79,3 @@
} }
} }
} }
.pool-distribution {
min-height: 56px;
display: block;
@media (min-width: 485px) {
display: flex;
flex-direction: row;
}
h5 {
margin-bottom: 10px;
}
.item {
width: 50%;
display: inline-block;
margin: 0px auto 20px;
&:nth-child(2) {
order: 2;
@media (min-width: 485px) {
order: 3;
}
}
&:nth-child(3) {
order: 3;
@media (min-width: 485px) {
order: 2;
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;
}
.card-text {
font-size: 18px;
span {
color: #ffffff66;
font-size: 12px;
}
}
}
}
.skeleton-loader {
width: 100%;
display: block;
max-width: 80px;
margin: 15px auto 3px;
}

View File

@ -0,0 +1,52 @@
<app-indexing-progress></app-indexing-progress>
<div class="full-container">
<div class="card-header mb-0 mb-md-4">
<span i18n="mining.block-prediction-accuracy">Block Predictions Accuracy</span>
<button class="btn" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
</button>
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 144">
<input ngbButton type="radio" [value]="'24h'" fragment="24h" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 24h
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 432">
<input ngbButton type="radio" [value]="'3d'" fragment="3d" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 3D
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 1008">
<input ngbButton type="radio" [value]="'1w'" fragment="1w" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 1W
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 4320">
<input ngbButton type="radio" [value]="'1m'" fragment="1m" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 1M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 12960">
<input ngbButton type="radio" [value]="'3m'" fragment="3m" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 3M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 25920">
<input ngbButton type="radio" [value]="'6m'" fragment="6m" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 6M
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 52560">
<input ngbButton type="radio" [value]="'1y'" fragment="1y" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 1Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 105120">
<input ngbButton type="radio" [value]="'2y'" fragment="2y" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 2Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> 3Y
</label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-predictions' | relativeUrl]"> ALL
</label>
</div>
</form>
</div>
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div>
</div>
</div>

View File

@ -0,0 +1,81 @@
.card-header {
border-bottom: 0;
font-size: 18px;
@media (min-width: 465px) {
font-size: 20px;
}
}
.main-title {
position: relative;
color: #ffffff91;
margin-top: -13px;
font-size: 10px;
text-transform: uppercase;
font-weight: 500;
text-align: center;
padding-bottom: 3px;
}
.full-container {
padding: 0px 15px;
width: 100%;
min-height: 500px;
height: calc(100% - 150px);
@media (max-width: 992px) {
height: 100%;
padding-bottom: 100px;
};
}
.chart {
width: 100%;
height: 100%;
padding-bottom: 20px;
padding-right: 10px;
@media (max-width: 992px) {
padding-bottom: 25px;
}
@media (max-width: 829px) {
padding-bottom: 50px;
}
@media (max-width: 767px) {
padding-bottom: 25px;
}
@media (max-width: 629px) {
padding-bottom: 55px;
}
@media (max-width: 567px) {
padding-bottom: 55px;
}
}
.chart-widget {
width: 100%;
height: 100%;
max-height: 270px;
}
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 991px) {
position: relative;
top: -65px;
}
@media (min-width: 830px) and (max-width: 991px) {
position: relative;
top: 0px;
}
@media (min-width: 830px) {
flex-direction: row;
float: right;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}

View File

@ -0,0 +1,289 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
import { EChartsOption } from 'echarts';
import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { formatNumber } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms';
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from 'src/app/shared/graphs.utils';
import { StorageService } from 'src/app/services/storage.service';
import { ActivatedRoute, Router } from '@angular/router';
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from 'src/app/services/state.service';
@Component({
selector: 'app-block-predictions-graph',
templateUrl: './block-predictions-graph.component.html',
styleUrls: ['./block-predictions-graph.component.scss'],
styles: [`
.loadingGraphs {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BlockPredictionsGraphComponent implements OnInit {
@Input() right: number | string = 45;
@Input() left: number | string = 75;
miningWindowPreference: string;
radioGroupForm: FormGroup;
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
};
statsObservable$: Observable<any>;
isLoading = true;
formatNumber = formatNumber;
timespan = '';
chartInstance: any = undefined;
constructor(
@Inject(LOCALE_ID) public locale: string,
private seoService: SeoService,
private apiService: ApiService,
private formBuilder: FormBuilder,
private storageService: StorageService,
private zone: NgZone,
private route: ActivatedRoute,
private stateService: StateService,
private router: Router,
) {
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
this.radioGroupForm.controls.dateSpan.setValue('1y');
}
ngOnInit(): void {
this.seoService.setTitle($localize`Block predictions accuracy`);
this.miningWindowPreference = '24h';//this.miningService.getDefaultTimespan('24h');
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
this.route
.fragment
.subscribe((fragment) => {
if (['24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) {
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
}
});
this.statsObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
.pipe(
startWith(this.radioGroupForm.controls.dateSpan.value),
switchMap((timespan) => {
this.storageService.setValue('miningWindowPreference', timespan);
this.timespan = timespan;
this.isLoading = true;
return this.apiService.getHistoricalBlockPrediction$(timespan)
.pipe(
tap((response) => {
this.prepareChartOptions(response.body);
this.isLoading = false;
}),
map((response) => {
return {
blockCount: parseInt(response.headers.get('x-total-count'), 10),
};
}),
);
}),
share()
);
}
prepareChartOptions(data) {
this.chartOptions = {
animation: false,
grid: {
top: 30,
bottom: 80,
right: this.right,
left: this.left,
},
tooltip: {
show: !this.isMobile(),
trigger: 'axis',
axisPointer: {
type: 'line'
},
backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: {
color: '#b1b1b1',
align: 'left',
},
borderColor: '#000',
formatter: (ticks) => {
let tooltip = `<b style="color: white; margin-left: 2px">${formatterXAxis(this.locale, this.timespan, parseInt(ticks[0].axisValue, 10) * 1000)}</b><br>`;
tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data.value, this.locale, '1.2-2')}%<br>`;
if (['24h', '3d'].includes(this.timespan)) {
tooltip += `<small>` + $localize`At block: ${ticks[0].data.block}` + `</small>`;
} else {
tooltip += `<small>` + $localize`Around block: ${ticks[0].data.block}` + `</small>`;
}
return tooltip;
}
},
xAxis: {
name: formatterXAxisLabel(this.locale, this.timespan),
nameLocation: 'middle',
nameTextStyle: {
padding: [10, 0, 0, 0],
},
type: 'category',
boundaryGap: false,
axisLine: { onZero: true },
axisLabel: {
formatter: val => formatterXAxisTimeCategory(this.locale, this.timespan, parseInt(val, 10)),
align: 'center',
fontSize: 11,
lineHeight: 12,
hideOverlap: true,
padding: [0, 5],
},
data: data.map(prediction => prediction[0])
},
yAxis: [
{
type: 'value',
axisLabel: {
color: 'rgb(110, 112, 121)',
formatter: (val) => {
return `${val}%`;
}
},
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
},
},
],
series: [
{
zlevel: 0,
name: $localize`Match rate`,
data: data.map(prediction => ({
value: prediction[2],
block: prediction[1],
itemStyle: {
color: this.getPredictionColor(prediction[2])
}
})),
type: 'bar',
barWidth: '90%',
},
],
dataZoom: [{
type: 'inside',
realtime: true,
zoomLock: true,
maxSpan: 100,
minSpan: 5,
moveOnMouseMove: false,
}, {
showDetail: false,
show: true,
type: 'slider',
brushSelect: false,
realtime: true,
left: 20,
right: 15,
selectedDataBackground: {
lineStyle: {
color: '#fff',
opacity: 0.45,
},
areaStyle: {
opacity: 0,
}
},
}],
};
}
colorGradient(fadeFraction, rgbColor1, rgbColor2, rgbColor3) {
let color1 = rgbColor1;
let color2 = rgbColor2;
let fade = fadeFraction;
// Do we have 3 colors for the gradient? Need to adjust the params.
if (rgbColor3) {
fade = fade * 2;
// Find which interval to use and adjust the fade percentage
if (fade >= 1) {
fade -= 1;
color1 = rgbColor2;
color2 = rgbColor3;
}
}
const diffRed = color2.red - color1.red;
const diffGreen = color2.green - color1.green;
const diffBlue = color2.blue - color1.blue;
const gradient = {
red: Math.floor(color1.red + (diffRed * fade)),
green: Math.floor(color1.green + (diffGreen * fade)),
blue: Math.floor(color1.blue + (diffBlue * fade)),
};
return 'rgb(' + gradient.red + ',' + gradient.green + ',' + gradient.blue + ')';
}
getPredictionColor(matchRate) {
return this.colorGradient(
Math.pow((100 - matchRate) / 100, 0.5),
{red: 67, green: 171, blue: 71},
{red: 253, green: 216, blue: 53},
{red: 244, green: 0, blue: 0},
);
}
onChartInit(ec) {
this.chartInstance = ec;
this.chartInstance.on('click', (e) => {
this.zone.run(() => {
if (['24h', '3d'].includes(this.timespan)) {
const url = new RelativeUrlPipe(this.stateService).transform(`/block/${e.data.block}`);
this.router.navigate([url]);
}
});
});
}
isMobile() {
return (window.innerWidth <= 767.98);
}
onSaveChart() {
// @ts-ignore
const prevBottom = this.chartOptions.grid.bottom;
const now = new Date();
// @ts-ignore
this.chartOptions.grid.bottom = 40;
this.chartOptions.backgroundColor = '#11131f';
this.chartInstance.setOption(this.chartOptions);
download(this.chartInstance.getDataURL({
pixelRatio: 2,
excludeComponents: ['dataZoom'],
}), `block-fees-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
// @ts-ignore
this.chartOptions.grid.bottom = prevBottom;
this.chartOptions.backgroundColor = 'none';
this.chartInstance.setOption(this.chartOptions);
}
}

View File

@ -36,7 +36,7 @@
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680"> <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 3Y <input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680"> <label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> ALL <input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]"> ALL
</label> </label>
</div> </div>

View File

@ -79,57 +79,3 @@
} }
} }
} }
.pool-distribution {
min-height: 56px;
display: block;
@media (min-width: 485px) {
display: flex;
flex-direction: row;
}
h5 {
margin-bottom: 10px;
}
.item {
width: 50%;
display: inline-block;
margin: 0px auto 20px;
&:nth-child(2) {
order: 2;
@media (min-width: 485px) {
order: 3;
}
}
&:nth-child(3) {
order: 3;
@media (min-width: 485px) {
order: 2;
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;
}
.card-text {
font-size: 18px;
span {
color: #ffffff66;
font-size: 12px;
}
}
}
}
.skeleton-loader {
width: 100%;
display: block;
max-width: 80px;
margin: 15px auto 3px;
}

View File

@ -35,7 +35,7 @@
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680"> <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 3Y <input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680"> <label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> ALL <input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]"> ALL
</label> </label>
</div> </div>

View File

@ -79,57 +79,3 @@
} }
} }
} }
.pool-distribution {
min-height: 56px;
display: block;
@media (min-width: 485px) {
display: flex;
flex-direction: row;
}
h5 {
margin-bottom: 10px;
}
.item {
width: 50%;
display: inline-block;
margin: 0px auto 20px;
&:nth-child(2) {
order: 2;
@media (min-width: 485px) {
order: 3;
}
}
&:nth-child(3) {
order: 3;
@media (min-width: 485px) {
order: 2;
display: block;
}
@media (min-width: 768px) {
display: none;
}
@media (min-width: 992px) {
display: block;
}
}
.card-title {
font-size: 1rem;
color: #4a68b9;
}
.card-text {
font-size: 18px;
span {
color: #ffffff66;
font-size: 12px;
}
}
}
}
.skeleton-loader {
width: 100%;
display: block;
max-width: 80px;
margin: 15px auto 3px;
}

View File

@ -50,7 +50,7 @@
<tbody> <tbody>
<tr> <tr>
<td class="td-width" i18n="block.hash">Hash</td> <td class="td-width" i18n="block.hash">Hash</td>
<td><a [routerLink]="['/block/' | relativeUrl, block.id]" title="{{ block.id }}">{{ block.id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.id"></app-clipboard></td> <td>&lrm;<a [routerLink]="['/block/' | relativeUrl, block.id]" title="{{ block.id }}">{{ block.id | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.id"></app-clipboard></td>
</tr> </tr>
<tr> <tr>
<td i18n="block.timestamp">Timestamp</td> <td i18n="block.timestamp">Timestamp</td>
@ -358,7 +358,7 @@
<div class="text-center"> <div class="text-center">
<span i18n="error.general-loading-data">Error loading data.</span> <span i18n="error.general-loading-data">Error loading data.</span>
<br><br> <br><br>
<i>{{ error.code }}: {{ error.error }}</i> <i>{{ error.status }}: {{ error.error }}</i>
</div> </div>
</ng-template> </ng-template>

View File

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ClipboardComponent } from './clipboard.component';
describe('ClipboardComponent', () => {
let component: ClipboardComponent;
let fixture: ComponentFixture<ClipboardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ClipboardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ClipboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -9,7 +9,7 @@
</tr> </tr>
</thead> </thead>
<tbody *ngIf="(hashrateObservable$ | async) as data"> <tbody *ngIf="(hashrateObservable$ | async) as data">
<tr *ngFor="let diffChange of data.difficulty"> <tr *ngFor="let diffChange of data">
<td class="d-none d-md-block"><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height <td class="d-none d-md-block"><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height
}}</a></td> }}</a></td>
<td class="text-left"> <td class="text-left">
@ -17,7 +17,7 @@
</td> </td>
<td class="text-right">{{ diffChange.difficultyShorten }}</td> <td class="text-right">{{ diffChange.difficultyShorten }}</td>
<td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'"> <td class="text-right" [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">
{{ diffChange.change >= 0 ? '+' : '' }}{{ diffChange.change | amountShortener }}% {{ diffChange.change >= 0 ? '+' : '' }}{{ diffChange.change | amountShortener: 2 }}%
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -30,27 +30,24 @@ export class DifficultyAdjustmentsTable implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.hashrateObservable$ = this.apiService.getHistoricalHashrate$('1y') this.hashrateObservable$ = this.apiService.getDifficultyAdjustments$('3m')
.pipe( .pipe(
map((response) => { map((response) => {
const data = response.body; const data = response.body;
const tableData = []; const tableData = [];
for (let i = data.difficulty.length - 1; i > 0; --i) { for (const adjustment of data) {
const selectedPowerOfTen: any = selectPowerOfTen(data.difficulty[i].difficulty); const selectedPowerOfTen: any = selectPowerOfTen(adjustment[2]);
const change = (data.difficulty[i].difficulty / data.difficulty[i - 1].difficulty - 1) * 100; tableData.push({
height: adjustment[1],
tableData.push(Object.assign(data.difficulty[i], { timestamp: adjustment[0],
change: Math.round(change * 100) / 100, change: (adjustment[3] - 1) * 100,
difficultyShorten: formatNumber( difficultyShorten: formatNumber(
data.difficulty[i].difficulty / selectedPowerOfTen.divider, adjustment[2] / selectedPowerOfTen.divider,
this.locale, '1.2-2') + selectedPowerOfTen.unit this.locale, '1.2-2') + selectedPowerOfTen.unit
})); });
} }
this.isLoading = false; this.isLoading = false;
return tableData.slice(0, 6);
return {
difficulty: tableData.slice(0, 6),
};
}), }),
); );
} }

View File

@ -1,4 +1,4 @@
<div *ngIf="stateService.env.MINING_DASHBOARD" class="mb-3 d-flex menu" style="padding: 0px 35px;"> <div *ngIf="stateService.env.MINING_DASHBOARD" class="mb-3 d-inline-flex menu" style="padding: 0px 35px;">
<a routerLinkActive="active" class="btn btn-primary w-50 mr-1" <a routerLinkActive="active" class="btn btn-primary w-50 mr-1"
[routerLink]="['/graphs/mempool' | relativeUrl]">Mempool</a> [routerLink]="['/graphs/mempool' | relativeUrl]">Mempool</a>
<div ngbDropdown class="w-50"> <div ngbDropdown class="w-50">
@ -18,6 +18,8 @@
[routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" i18n="mining.block-rewards">Block Rewards</a> [routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" i18n="mining.block-rewards">Block Rewards</a>
<a class="dropdown-item" routerLinkActive="active" <a class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">Block Sizes and Weights</a> [routerLink]="['/graphs/mining/block-sizes-weights' | relativeUrl]" i18n="mining.block-sizes-weights">Block Sizes and Weights</a>
<a class="dropdown-item" routerLinkActive="active"
[routerLink]="['/graphs/mining/block-predictions' | relativeUrl]" i18n="mining.block-prediction-accuracy">Blocks Predictions Accuracy</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -42,7 +42,7 @@
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680"> <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]"> 3Y <input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680"> <label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]"> ALL <input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]"> ALL
</label> </label>
</div> </div>

View File

@ -11,6 +11,7 @@ import { StorageService } from 'src/app/services/storage.service';
import { MiningService } from 'src/app/services/mining.service'; import { MiningService } from 'src/app/services/mining.service';
import { download } from 'src/app/shared/graphs.utils'; import { download } from 'src/app/shared/graphs.utils';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { StateService } from 'src/app/services/state.service';
@Component({ @Component({
selector: 'app-hashrate-chart', selector: 'app-hashrate-chart',
@ -47,7 +48,7 @@ export class HashrateChartComponent implements OnInit {
formatNumber = formatNumber; formatNumber = formatNumber;
timespan = ''; timespan = '';
chartInstance: any = undefined; chartInstance: any = undefined;
maResolution: number = 30; network = '';
constructor( constructor(
@Inject(LOCALE_ID) public locale: string, @Inject(LOCALE_ID) public locale: string,
@ -57,17 +58,20 @@ export class HashrateChartComponent implements OnInit {
private storageService: StorageService, private storageService: StorageService,
private miningService: MiningService, private miningService: MiningService,
private route: ActivatedRoute, private route: ActivatedRoute,
private stateService: StateService
) { ) {
} }
ngOnInit(): void { ngOnInit(): void {
this.stateService.networkChanged$.subscribe((network) => this.network = network);
let firstRun = true; let firstRun = true;
if (this.widget) { if (this.widget) {
this.miningWindowPreference = '1y'; this.miningWindowPreference = '1y';
} else { } else {
this.seoService.setTitle($localize`:@@3510fc6daa1d975f331e3a717bdf1a34efa06dff:Hashrate & Difficulty`); this.seoService.setTitle($localize`:@@3510fc6daa1d975f331e3a717bdf1a34efa06dff:Hashrate & Difficulty`);
this.miningWindowPreference = this.miningService.getDefaultTimespan('1m'); this.miningWindowPreference = this.miningService.getDefaultTimespan('3m');
} }
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
@ -95,6 +99,7 @@ export class HashrateChartComponent implements OnInit {
.pipe( .pipe(
tap((response) => { tap((response) => {
const data = response.body; const data = response.body;
// We generate duplicated data point so the tooltip works nicely // We generate duplicated data point so the tooltip works nicely
const diffFixed = []; const diffFixed = [];
let diffIndex = 1; let diffIndex = 1;
@ -112,7 +117,7 @@ export class HashrateChartComponent implements OnInit {
} }
while (hashIndex < data.hashrates.length && diffIndex < data.difficulty.length && while (hashIndex < data.hashrates.length && diffIndex < data.difficulty.length &&
data.hashrates[hashIndex].timestamp <= data.difficulty[diffIndex].timestamp data.hashrates[hashIndex].timestamp <= data.difficulty[diffIndex].time
) { ) {
diffFixed.push({ diffFixed.push({
timestamp: data.hashrates[hashIndex].timestamp, timestamp: data.hashrates[hashIndex].timestamp,
@ -123,17 +128,14 @@ export class HashrateChartComponent implements OnInit {
++diffIndex; ++diffIndex;
} }
this.maResolution = 30; let maResolution = 15;
if (["3m", "6m"].includes(this.timespan)) {
this.maResolution = 7;
}
const hashrateMa = []; const hashrateMa = [];
for (let i = this.maResolution - 1; i < data.hashrates.length; ++i) { for (let i = maResolution - 1; i < data.hashrates.length; ++i) {
let avg = 0; let avg = 0;
for (let y = this.maResolution - 1; y >= 0; --y) { for (let y = maResolution - 1; y >= 0; --y) {
avg += data.hashrates[i - y].avgHashrate; avg += data.hashrates[i - y].avgHashrate;
} }
avg /= this.maResolution; avg /= maResolution;
hashrateMa.push([data.hashrates[i].timestamp * 1000, avg]); hashrateMa.push([data.hashrates[i].timestamp * 1000, avg]);
} }
@ -275,17 +277,17 @@ export class HashrateChartComponent implements OnInit {
}, },
}, },
{ {
name: $localize`::Difficulty`, name: $localize`:@@25148835d92465353fc5fe8897c27d5369978e5a:Difficulty`,
inactiveColor: 'rgb(110, 112, 121)', inactiveColor: 'rgb(110, 112, 121)',
textStyle: { textStyle: {
color: 'white', color: 'white',
}, },
icon: 'roundRect', icon: 'roundRect',
}, },
{ {
name: $localize`Hashrate` + ` (MA${this.maResolution})`, name: $localize`Hashrate (MA)`,
inactiveColor: 'rgb(110, 112, 121)', inactiveColor: 'rgb(110, 112, 121)',
textStyle: { textStyle: {
color: 'white', color: 'white',
}, },
icon: 'roundRect', icon: 'roundRect',
@ -294,11 +296,18 @@ export class HashrateChartComponent implements OnInit {
}, },
}, },
], ],
selected: JSON.parse(this.storageService.getValue('hashrate_difficulty_legend')) ?? {
'$localize`:@@79a9dc5b1caca3cbeb1733a19515edacc5fc7920:Hashrate`': true,
'$localize`::Difficulty`': this.network === '',
'$localize`Hashrate (MA)`': true,
},
}, },
yAxis: data.hashrates.length === 0 ? undefined : [ yAxis: data.hashrates.length === 0 ? undefined : [
{ {
min: (value) => { min: (value) => {
return value.min * 0.9; const selectedPowerOfTen: any = selectPowerOfTen(value.min);
const newMin = Math.floor(value.min / selectedPowerOfTen.divider / 10);
return newMin * selectedPowerOfTen.divider * 10;
}, },
type: 'value', type: 'value',
axisLabel: { axisLabel: {
@ -362,7 +371,7 @@ export class HashrateChartComponent implements OnInit {
}, },
{ {
zlevel: 2, zlevel: 2,
name: $localize`Hashrate` + ` (MA${this.maResolution})`, name: $localize`Hashrate (MA)`,
showSymbol: false, showSymbol: false,
symbol: 'none', symbol: 'none',
data: data.hashrateMa, data: data.hashrateMa,
@ -403,6 +412,10 @@ export class HashrateChartComponent implements OnInit {
onChartInit(ec) { onChartInit(ec) {
this.chartInstance = ec; this.chartInstance = ec;
this.chartInstance.on('legendselectchanged', (e) => {
this.storageService.setValue('hashrate_difficulty_legend', JSON.stringify(e.selected));
});
} }
isMobile() { isMobile() {

View File

@ -21,7 +21,7 @@
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680"> <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]"> 3Y <input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.blockCount > 157680"> <label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]"> ALL <input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/pools-dominance' | relativeUrl]"> ALL
</label> </label>
</div> </div>

View File

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MempoolBlockComponent } from './mempool-block.component';
describe('MempoolBlockComponent', () => {
let component: MempoolBlockComponent;
let fixture: ComponentFixture<MempoolBlockComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MempoolBlockComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MempoolBlockComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -40,7 +40,7 @@
<div class="card"> <div class="card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2"> <div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<app-hashrate-chart [widget]="true"></app-hashrate-chart> <app-hashrate-chart [widget]="true"></app-hashrate-chart>
<div class="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div> <div class="mt-1"><a [routerLink]="['/graphs/mining/hashrate-difficulty' | relativeUrl]" fragment="1y" i18n="dashboard.view-more">View more &raquo;</a></div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -66,7 +66,7 @@
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 157680"> <label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount >= 157680">
<input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/pools' | relativeUrl]"> 3Y <input ngbButton type="radio" [value]="'3y'" fragment="3y" [routerLink]="['/graphs/mining/pools' | relativeUrl]"> 3Y
</label> </label>
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="stats.totalBlockCount > 157680"> <label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/pools' | relativeUrl]"><span i18n>All</span> <input ngbButton type="radio" [value]="'all'" fragment="all" [routerLink]="['/graphs/mining/pools' | relativeUrl]"><span i18n>All</span>
</label> </label>
</div> </div>

View File

@ -1,5 +1,5 @@
<div class="container-xl"> <div class="container-xl">
<h1 i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</h1> <h1 class="text-left" i18n="shared.broadcast-transaction|Broadcast Transaction">Broadcast Transaction</h1>
<form [formGroup]="pushTxForm" (submit)="pushTxForm.valid && postTx()" novalidate> <form [formGroup]="pushTxForm" (submit)="pushTxForm.valid && postTx()" novalidate>
<div class="mb-3"> <div class="mb-3">

View File

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { QrcodeComponent } from './qrcode.component';
describe('QrcodeComponent', () => {
let component: QrcodeComponent;
let fixture: ComponentFixture<QrcodeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ QrcodeComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(QrcodeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchFormComponent } from './search-form.component';
describe('SearchFormComponent', () => {
let component: SearchFormComponent;
let fixture: ComponentFixture<SearchFormComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ SearchFormComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { StartComponent } from './start.component';
describe('StartComponent', () => {
let component: StartComponent;
let fixture: ComponentFixture<StartComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ StartComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(StartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -66,9 +66,9 @@
</ng-template> </ng-template>
</ng-template> </ng-template>
<ng-template #defaultAddress> <ng-template #defaultAddress>
<a *ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}"> <a class="shortable-address" *ngIf="vin.prevout.scriptpubkey_address; else vinScriptPubkeyType" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vin.prevout.scriptpubkey_address | shortenString : 16 }}</span> <span class="d-block d-lg-none">{{ vin.prevout.scriptpubkey_address | shortenString : 16 }}</span>
<span class="d-none d-lg-flex justify-content-start"> <span class="d-none d-lg-inline-flex justify-content-start">
<span class="addr-left flex-grow-1" [style]="vin.prevout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vin.prevout.scriptpubkey_address }}</span> <span class="addr-left flex-grow-1" [style]="vin.prevout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vin.prevout.scriptpubkey_address }}</span>
<span *ngIf="vin.prevout.scriptpubkey_address.length > 40" class="addr-right">{{ vin.prevout.scriptpubkey_address | capAddress: 40: 10 }}</span> <span *ngIf="vin.prevout.scriptpubkey_address.length > 40" class="addr-right">{{ vin.prevout.scriptpubkey_address | capAddress: 40: 10 }}</span>
</span> </span>
@ -164,9 +164,9 @@
'highlight': vout.scriptpubkey_address === this.address && this.address !== '' 'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
}"> }">
<td> <td>
<a *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}"> <a class="shortable-address" *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vout.scriptpubkey_address | shortenString : 16 }}</span> <span class="d-block d-lg-none">{{ vout.scriptpubkey_address | shortenString : 16 }}</span>
<span class="d-none d-lg-flex justify-content-start"> <span class="d-none d-lg-inline-flex justify-content-start">
<span class="addr-left flex-grow-1" [style]="vout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vout.scriptpubkey_address }}</span> <span class="addr-left flex-grow-1" [style]="vout.scriptpubkey_address.length > 40 ? 'max-width: 235px' : ''">{{ vout.scriptpubkey_address }}</span>
<span *ngIf="vout.scriptpubkey_address.length > 40" class="addr-right">{{ vout.scriptpubkey_address | capAddress: 40: 10 }}</span> <span *ngIf="vout.scriptpubkey_address.length > 40" class="addr-right">{{ vout.scriptpubkey_address | capAddress: 40: 10 }}</span>
</span> </span>

View File

@ -6017,6 +6017,20 @@ export const faqData = [
fragment: "what-are-mining-pools", fragment: "what-are-mining-pools",
title: "What are mining pools?", title: "What are mining pools?",
}, },
{
type: "endpoint",
category: "basics",
showConditions: bitcoinNetworks,
fragment: "what-are-vb-wu",
title: "What are virtual bytes (vB) and weight units (WU)?",
},
{
type: "endpoint",
category: "basics",
showConditions: bitcoinNetworks,
fragment: "what-is-svb",
title: "What is sat/vB?",
},
{ {
type: "endpoint", type: "endpoint",
category: "basics", category: "basics",

View File

@ -17,15 +17,15 @@ export class ApiDocsNavComponent implements OnInit {
constructor() { } constructor() { }
ngOnInit(): void { ngOnInit(): void {
if( this.whichTab === 'rest' ) { if (this.whichTab === 'rest') {
this.tabData = restApiDocsData; this.tabData = restApiDocsData;
} else if( this.whichTab = 'faq' ) { } else if (this.whichTab === 'faq') {
this.tabData = faqData; this.tabData = faqData;
} }
} }
navLinkClick( event ) { navLinkClick(event) {
this.navLinkClickEvent.emit( event ); this.navLinkClickEvent.emit(event);
} }
} }

View File

@ -134,6 +134,19 @@
Mining pools are groups of miners that combine their computational power in order to increase the probability of finding new blocks. Mining pools are groups of miners that combine their computational power in order to increase the probability of finding new blocks.
</ng-template> </ng-template>
<ng-template type="what-are-vb-wu">
<p>Virtual bytes (vB) and weight units (WU) are used to measure the size of transactions and blocks on the Bitcoin network.</p>
<p>A Bitcoin transaction's size in the blockchain is <i>not</i> determined how much bitcoin it transfers—instead, a transaction's size is determined by technical factors such as how many inputs and outputs it has, how many signatures it has, and the format it uses (legacy, SegWit, etc). Since space in the Bitcoin blockchain is limited, bigger transactions pay more in mining fees than smaller transactions.</p>
<p>Block sizes are limited to 4,000,000 WU (or 1,000,000 vB since 1 vB = 4 WU).</p>
<p>Transaction sizes and block sizes used to be measured in plain bytes, but virtual bytes and weight units were devised to maintain backward compatibility after the SegWit upgrade in 2017. See <a href="https://programmingbitcoin.com/understanding-segwit-block-size" target="_blank">this post</a> for more details.</p>
</ng-template>
<ng-template type="what-is-svb">
<p>The priority of a pending Bitcoin transaction is determined by its feerate. Feerates are measured in sat/vB.</p>
<p>Using a higher sat/vB feerate for a Bitcoin transaction will generally result in quicker confirmation than using a lower feerate. But feerates change all the time, so it's important to check suggested feerates right before making a transaction to <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="why-is-transaction-stuck-in-mempool">avoid it from getting stuck</a>.</p>
<p>There are feerate estimates on the top of <a [routerLink]="['/' | relativeUrl]">the main dashboard</a> you can use as a guide. See <a [routerLink]="['/docs/faq' | relativeUrl]" fragment="looking-up-fee-estimates">this FAQ</a> for more on picking the right feerate.</p>
</ng-template>
<ng-template type="what-is-full-mempool"> <ng-template type="what-is-full-mempool">
<p>When a Bitcoin transaction is made, it is stored in a Bitcoin node's mempool before it is confirmed into a block. When the rate of incoming transactions exceeds the rate transactions are confirmed, the mempool grows in size.</p><p>The default maximum size of a Bitcoin node's mempool is 300MB, so when there are 300MB of transactions in the mempool, we say it's "full".</p> <p>When a Bitcoin transaction is made, it is stored in a Bitcoin node's mempool before it is confirmed into a block. When the rate of incoming transactions exceeds the rate transactions are confirmed, the mempool grows in size.</p><p>The default maximum size of a Bitcoin node's mempool is 300MB, so when there are 300MB of transactions in the mempool, we say it's "full".</p>
</ng-template> </ng-template>

View File

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FiatComponent } from './fiat.component';
describe('FiatComponent', () => {
let component: FiatComponent;
let fixture: ComponentFixture<FiatComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ FiatComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FiatComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -22,6 +22,7 @@ import { DashboardComponent } from '../dashboard/dashboard.component';
import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component'; import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component';
import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-chart.component'; import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-chart.component';
import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools/hashrate-chart-pools.component'; import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools/hashrate-chart-pools.component';
import { BlockPredictionsGraphComponent } from '../components/block-predictions-graph/block-predictions-graph.component';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@NgModule({ @NgModule({
@ -47,6 +48,7 @@ import { CommonModule } from '@angular/common';
LbtcPegsGraphComponent, LbtcPegsGraphComponent,
HashrateChartComponent, HashrateChartComponent,
HashrateChartPoolsComponent, HashrateChartPoolsComponent,
BlockPredictionsGraphComponent,
], ],
imports: [ imports: [
CommonModule, CommonModule,

View File

@ -1,5 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { BlockPredictionsGraphComponent } from '../components/block-predictions-graph/block-predictions-graph.component';
import { BlockFeeRatesGraphComponent } from '../components/block-fee-rates-graph/block-fee-rates-graph.component'; import { BlockFeeRatesGraphComponent } from '../components/block-fee-rates-graph/block-fee-rates-graph.component';
import { BlockFeesGraphComponent } from '../components/block-fees-graph/block-fees-graph.component'; import { BlockFeesGraphComponent } from '../components/block-fees-graph/block-fees-graph.component';
import { BlockRewardsGraphComponent } from '../components/block-rewards-graph/block-rewards-graph.component'; import { BlockRewardsGraphComponent } from '../components/block-rewards-graph/block-rewards-graph.component';
@ -92,6 +93,10 @@ const routes: Routes = [
path: '', path: '',
redirectTo: 'mempool', redirectTo: 'mempool',
}, },
{
path: 'mining/block-predictions',
component: BlockPredictionsGraphComponent,
},
] ]
}, },
{ {

View File

@ -140,7 +140,7 @@ export class ApiService {
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools` + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools` +
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' } (interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
); );
} }
getPoolStats$(slug: string): Observable<PoolStat> { getPoolStats$(slug: string): Observable<PoolStat> {
return this.httpClient.get<PoolStat>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${slug}`); return this.httpClient.get<PoolStat>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pool/${slug}`);
@ -172,6 +172,13 @@ export class ApiService {
return this.httpClient.get<TransactionStripped[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/summary'); return this.httpClient.get<TransactionStripped[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/block/' + hash + '/summary');
} }
getDifficultyAdjustments$(interval: string | undefined): Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/difficulty-adjustments` +
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
);
}
getHistoricalHashrate$(interval: string | undefined): Observable<any> { getHistoricalHashrate$(interval: string | undefined): Observable<any> {
return this.httpClient.get<any[]>( return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate` +
@ -214,6 +221,13 @@ export class ApiService {
); );
} }
getHistoricalBlockPrediction$(interval: string | undefined) : Observable<any> {
return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/predictions` +
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
);
}
getRewardStats$(blockCount: number = 144): Observable<RewardStats> { getRewardStats$(blockCount: number = 144): Observable<RewardStats> {
return this.httpClient.get<RewardStats>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`); return this.httpClient.get<RewardStats>(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`);
} }

View File

@ -1,8 +0,0 @@
import { AbsolutePipe } from './absolute.pipe';
describe('AbsolutePipe', () => {
it('create an instance', () => {
const pipe = new AbsolutePipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -1,8 +0,0 @@
import { AsmStylerPipe } from './asm-styler.pipe';
describe('OpcodesStylerPipe', () => {
it('create an instance', () => {
const pipe = new AsmStylerPipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -1,8 +0,0 @@
import { FeeRoundingPipe } from './fee-rounding.pipe';
describe('FeeRoundingPipe', () => {
it('create an instance', () => {
const pipe = new FeeRoundingPipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -1,8 +0,0 @@
import { Hex2asciiPipe } from './hex2ascii.pipe';
describe('Hex2asciiPipe', () => {
it('create an instance', () => {
const pipe = new Hex2asciiPipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -1,8 +0,0 @@
import { RelativeUrlPipe } from './relative-url.pipe';
describe('RelativeUrlPipe', () => {
it('create an instance', () => {
const pipe = new RelativeUrlPipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -852,6 +852,40 @@ th {
} }
} }
.fee-progress-bar {
@extend .fee-progress-bar;
&.priority {
@media (767px < width < 992px), (width < 576px) {
width: 100%;
}
width: 75%;
border-radius: 10px 0px 0px 10px !important;
}
}
.fees-wrapper-tooltip-chart {
@extend .fees-wrapper-tooltip-chart;
.title {
direction: rtl;
}
}
.btn-link {
padding: 0.1rem 0.5rem 0.25rem 0 !important;
}
.shortable-address {
direction: ltr;
}
.lastest-blocks-table {
@extend .lastest-blocks-table;
.table-cell-mined {
@extend .table-cell-mined;
text-align: right !important;
}
}
.mempool-graph { .mempool-graph {
@extend .mempool-graph; @extend .mempool-graph;
direction: ltr; direction: ltr;

View File

@ -1,22 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(), {
teardown: { destroyAfterEach: false }
}
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@ -1,158 +0,0 @@
{
"extends": "tslint:recommended",
"rules": {
"align": {
"options": [
"parameters",
"statements"
]
},
"array-type": false,
"forin": false,
"arrow-parens": false,
"arrow-return-shorthand": true,
"curly": true,
"no-bitwise": false,
"deprecation": {
"severity": "warning"
},
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"eofline": true,
"import-spacing": true,
"indent": {
"options": [
"spaces"
]
},
"object-literal-shorthand": false,
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
],
"import-blacklist": [
true,
"rxjs/Rx"
],
"interface-name": false,
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-consecutive-blank-lines": false,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-empty": false,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [
false,
"as-needed"
],
"object-literal-sort-keys": false,
"ordered-imports": false,
"quotemark": [
true,
"single"
],
"semicolon": {
"options": [
"always"
]
},
"space-before-function-paren": {
"options": {
"anonymous": "never",
"asyncArrow": "always",
"constructor": "never",
"method": "never",
"named": "never"
}
},
"trailing-comma": false,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"typedef-whitespace": {
"options": [
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
]
},
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true
, "variable-name": {
"options": [
"ban-keywords",
"check-format",
"allow-pascal-case"
]
},
"whitespace": {
"options": [
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type",
"check-typecast"
]
}
},
"rulesDirectory": [
"codelyzer"
]
}

View File

@ -1,4 +1,4 @@
#!/usr/local/bin/zsh #!/usr/bin/env zsh
cd "${HOME}/electrs" cd "${HOME}/electrs"
#source "${HOME}/.cargo/env" #source "${HOME}/.cargo/env"
#export PATH="${HOME}/.cargo/bin:${PATH}" #export PATH="${HOME}/.cargo/bin:${PATH}"

View File

@ -1,4 +1,4 @@
#!/usr/local/bin/zsh #!/usr/bin/env zsh
cd "${HOME}/electrs" cd "${HOME}/electrs"
#source "${HOME}/.cargo/env" #source "${HOME}/.cargo/env"
#export PATH="${HOME}/.cargo/bin:${PATH}" #export PATH="${HOME}/.cargo/bin:${PATH}"

View File

@ -1,4 +1,4 @@
#!/usr/local/bin/zsh #!/usr/bin/env zsh
cd "${HOME}/electrs" cd "${HOME}/electrs"
#source "${HOME}/.cargo/env" #source "${HOME}/.cargo/env"
#export PATH="${HOME}/.cargo/bin:${PATH}" #export PATH="${HOME}/.cargo/bin:${PATH}"

View File

@ -1,4 +1,4 @@
#!/usr/local/bin/zsh #!/usr/bin/env zsh
cd "${HOME}/electrs" cd "${HOME}/electrs"
#source $HOME/.cargo/env #source $HOME/.cargo/env
#export PATH=$HOME/.cargo/bin:$PATH #export PATH=$HOME/.cargo/bin:$PATH

View File

@ -6,11 +6,13 @@ case `uname -s` in
FreeBSD) FreeBSD)
OS=FreeBSD OS=FreeBSD
NPROC=$(sysctl hw.ncpu | awk '{print $2}')
;; ;;
Linux) Linux)
if [ "$(grep -Ei 'debian|buntu|mint' /etc/*release)" ]; then if [ "$(grep -Ei 'debian|buntu|mint' /etc/*release)" ]; then
OS=Debian OS=Debian
NPROC=$(nproc --all)
else else
echo "Your distribution of Linux is not yet supported by this installation script" echo "Your distribution of Linux is not yet supported by this installation script"
exit 1 exit 1
@ -39,6 +41,7 @@ ELEMENTS_INSTALL=ON
# configure 4 network instances # configure 4 network instances
BITCOIN_MAINNET_ENABLE=ON BITCOIN_MAINNET_ENABLE=ON
BITCOIN_MAINNET_MINFEE_ENABLE=ON
BITCOIN_TESTNET_ENABLE=ON BITCOIN_TESTNET_ENABLE=ON
BITCOIN_SIGNET_ENABLE=ON BITCOIN_SIGNET_ENABLE=ON
BISQ_MAINNET_ENABLE=ON BISQ_MAINNET_ENABLE=ON
@ -191,9 +194,9 @@ case $OS in
TOR_CONFIGURATION=/etc/tor/torrc TOR_CONFIGURATION=/etc/tor/torrc
TOR_RESOURCES=/var/lib/tor TOR_RESOURCES=/var/lib/tor
TOR_PKG=tor TOR_PKG=tor
TOR_USER=tor-debian TOR_USER=debian-tor
TOR_GROUP=tor-debian TOR_GROUP=debian-tor
CERTBOT_PKG=python-certbot CERTBOT_PKG=python3-certbot-nginx
NGINX_CONFIGURATION=/etc/nginx/nginx.conf NGINX_CONFIGURATION=/etc/nginx/nginx.conf
;; ;;
esac esac
@ -320,12 +323,12 @@ LIQUIDTESTNET_ASSET_REGISTRY_DB_NAME=asset_registry_testnet_db
# packages needed for mempool ecosystem # packages needed for mempool ecosystem
DEBIAN_PKG=() DEBIAN_PKG=()
DEBIAN_PKG+=(zsh vim curl screen openssl python3 dialog) DEBIAN_PKG+=(zsh vim curl screen openssl python3 dialog cron)
DEBIAN_PKG+=(build-essential git git-lfs clang cmake jq) DEBIAN_PKG+=(build-essential git git-lfs clang cmake jq)
DEBIAN_PKG+=(autotools-dev autoconf automake pkg-config bsdmainutils) DEBIAN_PKG+=(autotools-dev autoconf automake pkg-config bsdmainutils)
DEBIAN_PKG+=(libevent-dev libdb-dev libssl-dev libtool-dev autotools-dev) DEBIAN_PKG+=(libevent-dev libdb-dev libssl-dev libtool autotools-dev)
DEBIAN_PKG+=(libboost-system-dev libboost-filesystem-dev libboost-chrono-dev libboost-test-dev libboost-thread-dev) DEBIAN_PKG+=(libboost-system-dev libboost-filesystem-dev libboost-chrono-dev libboost-test-dev libboost-thread-dev)
DEBIAN_PKG+=(nodejs npm mariadb-server nginx-core python-certbot-nginx rsync ufw) DEBIAN_PKG+=(nodejs npm mariadb-server nginx-core python3-certbot-nginx rsync ufw)
# packages needed for mempool ecosystem # packages needed for mempool ecosystem
FREEBSD_PKG=() FREEBSD_PKG=()
@ -555,7 +558,6 @@ zfsCreateFilesystems()
ext4CreateDir() ext4CreateDir()
{ {
mkdir -p "/backup" "${ELEMENTS_HOME}" "${BITCOIN_HOME}" "${MINFEE_HOME}" "${ELECTRS_HOME}" "${MEMPOOL_HOME}" "${MYSQL_HOME}" "${BITCOIN_ELECTRS_HOME}" "${ELEMENTS_HOME}/liquidv1" "${ELEMENTS_ELECTRS_HOME}" mkdir -p "/backup" "${ELEMENTS_HOME}" "${BITCOIN_HOME}" "${MINFEE_HOME}" "${ELECTRS_HOME}" "${MEMPOOL_HOME}" "${MYSQL_HOME}" "${BITCOIN_ELECTRS_HOME}" "${ELEMENTS_HOME}/liquidv1" "${ELEMENTS_ELECTRS_HOME}"
exit
# Bitcoin Mainnet # Bitcoin Mainnet
if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
for folder in chainstate indexes blocks for folder in chainstate indexes blocks
@ -683,6 +685,7 @@ $CUT >$input <<-EOF
Tor:Enable Tor v3 HS Onion:ON Tor:Enable Tor v3 HS Onion:ON
Certbot:Enable HTTPS using Certbot:ON Certbot:Enable HTTPS using Certbot:ON
Mainnet:Enable Bitcoin Mainnet:ON Mainnet:Enable Bitcoin Mainnet:ON
Mainnet-Minfee:Enable Bitcoin Mainnet Minfee:ON
Testnet:Enable Bitcoin Testnet:ON Testnet:Enable Bitcoin Testnet:ON
Liquid:Enable Elements Liquid:ON Liquid:Enable Elements Liquid:ON
Bisq:Enable Bisq:ON Bisq:Enable Bisq:ON
@ -726,6 +729,12 @@ else
BITCOIN_MAINNET_ENABLE=OFF BITCOIN_MAINNET_ENABLE=OFF
fi fi
if grep Mainnet-Minfee $tempfile >/dev/null 2>&1;then
BITCOIN_MAINNET_MINFEE_ENABLE=ON
else
BITCOIN_MAINNET_MINFEE_ENABLE=OFF
fi
if grep Testnet $tempfile >/dev/null 2>&1;then if grep Testnet $tempfile >/dev/null 2>&1;then
BITCOIN_TESTNET_ENABLE=ON BITCOIN_TESTNET_ENABLE=ON
else else
@ -965,7 +974,7 @@ if [ "${BITCOIN_INSTALL}" = ON ];then
echo "[*] Building Bitcoin from source repo" echo "[*] Building Bitcoin from source repo"
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_REPO_NAME} && ./autogen.sh --quiet" osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_REPO_NAME} && ./autogen.sh --quiet"
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_REPO_NAME} && MAKE=gmake CC=cc CXX=c++ CPPFLAGS=-I/usr/local/include ./configure --with-gui=no --disable-wallet --disable-tests" osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_REPO_NAME} && MAKE=gmake CC=cc CXX=c++ CPPFLAGS=-I/usr/local/include ./configure --with-gui=no --disable-wallet --disable-tests"
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_REPO_NAME} && gmake -j48" osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_REPO_NAME} && gmake -j${NPROC}"
echo "[*] Installing Bitcoin binaries into OS" echo "[*] Installing Bitcoin binaries into OS"
osSudo "${ROOT_USER}" sh -c "cd ${BITCOIN_HOME}/${BITCOIN_REPO_NAME} && gmake install" osSudo "${ROOT_USER}" sh -c "cd ${BITCOIN_HOME}/${BITCOIN_REPO_NAME} && gmake install"
@ -977,10 +986,10 @@ if [ "${BITCOIN_INSTALL}" = ON ];then
osSudo "${ROOT_USER}" install -c -o "${MINFEE_USER}" -g "${MINFEE_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin.minfee.conf" "${MINFEE_HOME}/bitcoin.conf" osSudo "${ROOT_USER}" install -c -o "${MINFEE_USER}" -g "${MINFEE_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin.minfee.conf" "${MINFEE_HOME}/bitcoin.conf"
echo "[*] Installing Bitcoin RPC credentials" echo "[*] Installing Bitcoin RPC credentials"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_HOME}/bitcoin.conf" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_HOME}/bitcoin.conf"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_HOME}/bitcoin.conf" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_HOME}/bitcoin.conf"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${MINFEE_HOME}/bitcoin.conf" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${MINFEE_HOME}/bitcoin.conf"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${MINFEE_HOME}/bitcoin.conf" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${MINFEE_HOME}/bitcoin.conf"
fi fi
######################### #########################
@ -1010,7 +1019,7 @@ if [ "${ELEMENTS_INSTALL}" = ON ];then
echo "[*] Building Elements from source repo" echo "[*] Building Elements from source repo"
osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_REPO_NAME} && ./autogen.sh --quiet" osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_REPO_NAME} && ./autogen.sh --quiet"
osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_REPO_NAME} && MAKE=gmake CC=cc CXX=c++ CPPFLAGS=-I/usr/local/include ./configure --with-gui=no --disable-wallet --disable-tests" osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_REPO_NAME} && MAKE=gmake CC=cc CXX=c++ CPPFLAGS=-I/usr/local/include ./configure --with-gui=no --disable-wallet --disable-tests"
osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_REPO_NAME} && gmake -j48" osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_REPO_NAME} && gmake -j${NPROC}"
echo "[*] Installing Elements binaries into OS" echo "[*] Installing Elements binaries into OS"
osSudo "${ROOT_USER}" sh -c "cd ${ELEMENTS_HOME}/${ELEMENTS_REPO_NAME} && gmake install" osSudo "${ROOT_USER}" sh -c "cd ${ELEMENTS_HOME}/${ELEMENTS_REPO_NAME} && gmake install"
@ -1019,10 +1028,10 @@ if [ "${ELEMENTS_INSTALL}" = ON ];then
osSudo "${ROOT_USER}" install -c -o "${ELEMENTS_USER}" -g "${ELEMENTS_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/elements.conf" "${ELEMENTS_HOME}/elements.conf" osSudo "${ROOT_USER}" install -c -o "${ELEMENTS_USER}" -g "${ELEMENTS_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/elements.conf" "${ELEMENTS_HOME}/elements.conf"
echo "[*] Configuring Elements Liquid RPC credentials in elements.conf" echo "[*] Configuring Elements Liquid RPC credentials in elements.conf"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${ELEMENTS_HOME}/elements.conf" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${ELEMENTS_HOME}/elements.conf"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${ELEMENTS_HOME}/elements.conf" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${ELEMENTS_HOME}/elements.conf"
osSudo "${ROOT_USER}" sed -i .orig "s/__ELEMENTS_RPC_USER__/${ELEMENTS_RPC_USER}/" "${ELEMENTS_HOME}/elements.conf" osSudo "${ROOT_USER}" sed -i.orig "s/__ELEMENTS_RPC_USER__/${ELEMENTS_RPC_USER}/" "${ELEMENTS_HOME}/elements.conf"
osSudo "${ROOT_USER}" sed -i .orig "s/__ELEMENTS_RPC_PASS__/${ELEMENTS_RPC_PASS}/" "${ELEMENTS_HOME}/elements.conf" osSudo "${ROOT_USER}" sed -i.orig "s/__ELEMENTS_RPC_PASS__/${ELEMENTS_RPC_PASS}/" "${ELEMENTS_HOME}/elements.conf"
fi fi
################################### ###################################
@ -1049,17 +1058,23 @@ case $OS in
;; ;;
Debian) Debian)
echo "[*] Installing Rust from rustup.rs" echo "[*] Installing Rust from rustup.rs"
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"
;; ;;
esac esac
echo "[*] Building Bitcoin Electrs release binary" echo "[*] Building Bitcoin Electrs release binary"
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && cargo run --release --bin electrs -- --version" || true osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && cargo run --release --bin electrs -- --version" || true
echo "[*] Patching Bitcoin Electrs code for FreeBSD" case $OS in
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_HOME}/.cargo/registry/src/github.com-1ecc6299db9ec823/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\"" FreeBSD)
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/new_index/\" && sed -i .bak -e s/Snappy/None/ db.rs && rm db.rs.bak" echo "[*] Patching Bitcoin Electrs code for FreeBSD"
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/bin/\" && sed -i .bak -e 's/from_secs(5)/from_secs(1)/' electrs.rs && rm electrs.rs.bak" osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_HOME}/.cargo/registry/src/github.com-1ecc6299db9ec823/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\""
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/new_index/\" && sed -i.bak -e s/Snappy/None/ db.rs && rm db.rs.bak"
osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/bin/\" && sed -i.bak -e 's/from_secs(5)/from_secs(1)/' electrs.rs && rm electrs.rs.bak"
;;
Debian)
;;
esac
echo "[*] Building Bitcoin Electrs release binary" echo "[*] Building Bitcoin Electrs release binary"
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && cargo run --release --bin electrs -- --version" osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && cargo run --release --bin electrs -- --version"
@ -1090,20 +1105,17 @@ echo "[*] Cloning Liquid Asset Registry testnet repo from ${LIQUIDTESTNET_ASSET_
osSudo "${ELEMENTS_USER}" git config --global advice.detachedHead false osSudo "${ELEMENTS_USER}" git config --global advice.detachedHead false
osSudo "${ELEMENTS_USER}" git clone "${LIQUIDTESTNET_ASSET_REGISTRY_DB_URL}" "${ELEMENTS_HOME}/${LIQUIDTESTNET_ASSET_REGISTRY_DB_NAME}" osSudo "${ELEMENTS_USER}" git clone "${LIQUIDTESTNET_ASSET_REGISTRY_DB_URL}" "${ELEMENTS_HOME}/${LIQUIDTESTNET_ASSET_REGISTRY_DB_NAME}"
case $OS in
FreeBSD)
;;
Debian)
echo "[*] Installing Rust from rustup.rs"
osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
;;
esac
echo "[*] Building Liquid Electrs release binary" echo "[*] Building Liquid Electrs release binary"
osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_ELECTRS_HOME} && cargo run --release --features liquid --bin electrs -- --network liquid --version" || true osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_ELECTRS_HOME} && cargo run --release --features liquid --bin electrs -- --network liquid --version" || true
echo "[*] Patching Liquid Electrs code for FreeBSD" case $OS in
osSudo "${ELEMENTS_USER}" sh -c "cd \"${ELEMENTS_HOME}/.cargo/registry/src/github.com-1ecc6299db9ec823/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\"" FreeBSD)
echo "[*] Patching Liquid Electrs code for FreeBSD"
osSudo "${ELEMENTS_USER}" sh -c "cd \"${ELEMENTS_HOME}/.cargo/registry/src/github.com-1ecc6299db9ec823/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\""
;;
Debian)
;;
esac
echo "[*] Building Liquid Electrs release binary" echo "[*] Building Liquid Electrs release binary"
osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_ELECTRS_HOME} && cargo run --release --features liquid --bin electrs -- --network liquid --version" || true osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_ELECTRS_HOME} && cargo run --release --features liquid --bin electrs -- --network liquid --version" || true
@ -1135,8 +1147,8 @@ if [ "${BISQ_INSTALL}" = ON ];then
echo "[*] Cloning Bisq top-level repo" echo "[*] Cloning Bisq top-level repo"
osSudo "${BISQ_USER}" git clone --branch "${BISQ_REPO_BRANCH}" "${BISQ_REPO_URL}" "${BISQ_HOME}/${BISQ_REPO_NAME}" osSudo "${BISQ_USER}" git clone --branch "${BISQ_REPO_BRANCH}" "${BISQ_REPO_URL}" "${BISQ_HOME}/${BISQ_REPO_NAME}"
echo "[*] Installing OpenJDK 10.0.2 from Bisq install_java.sh script" echo "[*] Installing OpenJDK from Bisq install_java_linux.sh script"
osSudo "${ROOT_USER}" "${BISQ_HOME}/${BISQ_REPO_NAME}/scripts/install_java.sh" osSudo "${ROOT_USER}" "${BISQ_HOME}/${BISQ_REPO_NAME}/scripts/install_java_linux.sh"
echo "[*] Checking out Bisq ${BISQ_LATEST_RELEASE}" echo "[*] Checking out Bisq ${BISQ_LATEST_RELEASE}"
osSudo "${BISQ_USER}" sh -c "cd ${BISQ_HOME}/${BISQ_REPO_NAME} && git checkout ${BISQ_LATEST_RELEASE}" osSudo "${BISQ_USER}" sh -c "cd ${BISQ_HOME}/${BISQ_REPO_NAME} && git checkout ${BISQ_LATEST_RELEASE}"
@ -1159,26 +1171,26 @@ if [ "${BISQ_INSTALL}" = ON ];then
Debian) Debian)
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${BISQ_HOME}/${BISQ_REPO_NAME}/seednode/bisq.service" "${DEBIAN_SERVICE_HOME}/bisq.service" osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${BISQ_HOME}/${BISQ_REPO_NAME}/seednode/bisq.service" "${DEBIAN_SERVICE_HOME}/bisq.service"
osSudo "${ROOT_USER}" sed -i .orig "s/#Requires=bitcoin.service/Requires=bitcoin.service/" "${DEBIAN_SERVICE_HOME}/bisq.service" osSudo "${ROOT_USER}" sed -i.orig "s/#Requires=bitcoin.service/Requires=bitcoin.service/" "${DEBIAN_SERVICE_HOME}/bisq.service"
osSudo "${ROOT_USER}" sed -i .orig "s/#BindsTo=bitcoin.service/BindsTo=bitcoin.service/" "${DEBIAN_SERVICE_HOME}/bisq.service" osSudo "${ROOT_USER}" sed -i.orig "s/#BindsTo=bitcoin.service/BindsTo=bitcoin.service/" "${DEBIAN_SERVICE_HOME}/bisq.service"
osSudo "${ROOT_USER}" sed -i .orig "s/__BISQ_REPO_NAME__/${BISQ_REPO_NAME}/" "${DEBIAN_SERVICE_HOME}/bisq.service" osSudo "${ROOT_USER}" sed -i.orig "s/__BISQ_REPO_NAME__/${BISQ_REPO_NAME}/" "${DEBIAN_SERVICE_HOME}/bisq.service"
osSudo "${ROOT_USER}" sed -i .orig "s!__BISQ_HOME__!${BISQ_HOME}!" "${DEBIAN_SERVICE_HOME}/bisq.service" osSudo "${ROOT_USER}" sed -i.orig "s!__BISQ_HOME__!${BISQ_HOME}!" "${DEBIAN_SERVICE_HOME}/bisq.service"
echo "[*] Installing Bisq environment file" echo "[*] Installing Bisq environment file"
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${BISQ_HOME}/${BISQ_REPO_NAME}/seednode/bisq.env" "${DEBIAN_ENV_HOME}/bisq.env" osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${BISQ_HOME}/${BISQ_REPO_NAME}/seednode/bisq.env" "${DEBIAN_ENV_HOME}/bisq.env"
osSudo "${ROOT_USER}" sed -i .orig "s!__BISQ_APP_NAME__!${BISQ_APP_NAME}!" "${DEBIAN_ENV_HOME}/bisq.env" osSudo "${ROOT_USER}" sed -i.orig "s!__BISQ_APP_NAME__!${BISQ_APP_NAME}!" "${DEBIAN_ENV_HOME}/bisq.env"
osSudo "${ROOT_USER}" sed -i .orig "s!__BISQ_HOME__!${BISQ_HOME}!" "${DEBIAN_ENV_HOME}/bisq.env" osSudo "${ROOT_USER}" sed -i.orig "s!__BISQ_HOME__!${BISQ_HOME}!" "${DEBIAN_ENV_HOME}/bisq.env"
echo "[*] Configuring Bisq environment file with Bitcoin RPC credentials" echo "[*] Configuring Bisq environment file with Bitcoin RPC credentials"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_P2P_HOST__/${BITCOIN_MAINNET_P2P_HOST}/" "${DEBIAN_ENV_HOME}/bisq.env" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_P2P_HOST__/${BITCOIN_MAINNET_P2P_HOST}/" "${DEBIAN_ENV_HOME}/bisq.env"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_P2P_PORT__/${BITCOIN_MAINNET_P2P_PORT}/" "${DEBIAN_ENV_HOME}/bisq.env" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_P2P_PORT__/${BITCOIN_MAINNET_P2P_PORT}/" "${DEBIAN_ENV_HOME}/bisq.env"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_HOST__/${BITCOIN_MAINNET_RPC_HOST}/" "${DEBIAN_ENV_HOME}/bisq.env" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_HOST__/${BITCOIN_MAINNET_RPC_HOST}/" "${DEBIAN_ENV_HOME}/bisq.env"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_PORT__/${BITCOIN_MAINNET_RPC_PORT}/" "${DEBIAN_ENV_HOME}/bisq.env" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PORT__/${BITCOIN_MAINNET_RPC_PORT}/" "${DEBIAN_ENV_HOME}/bisq.env"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${DEBIAN_ENV_HOME}/bisq.env" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${DEBIAN_ENV_HOME}/bisq.env"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${DEBIAN_ENV_HOME}/bisq.env" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${DEBIAN_ENV_HOME}/bisq.env"
#echo "[*] Updating Bitcoin configuration for Bisq" #echo "[*] Updating Bitcoin configuration for Bisq"
#osSudo "${ROOT_USER}" sed -i .orig "s/#blocknotify/blocknotify/" "${BITCOIN_HOME}/bitcoin.conf" #osSudo "${ROOT_USER}" sed -i.orig "s/#blocknotify/blocknotify/" "${BITCOIN_HOME}/bitcoin.conf"
#osSudo "${BITCOIN_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${BISQ_HOME}/${BISQ_REPO_NAME}/seednode/blocknotify.sh" "${BITCOIN_HOME}/blocknotify.sh" #osSudo "${BITCOIN_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${BISQ_HOME}/${BISQ_REPO_NAME}/seednode/blocknotify.sh" "${BITCOIN_HOME}/blocknotify.sh"
;; ;;
@ -1198,7 +1210,79 @@ if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
;; ;;
Debian) Debian)
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin-mainnet.service" "${DEBIAN_SERVICE_HOME}" osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/linux/bitcoin.service" "${DEBIAN_SERVICE_HOME}"
;;
esac
fi
#######################################
# Bitcoin instance for Mainnet Minfee #
#######################################
if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
echo "[*] Installing Bitcoin Minfee service"
case $OS in
FreeBSD)
echo "[*] FIXME: Bitcoin Minfee service must be installed manually on FreeBSD"
;;
Debian)
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/linux/bitcoin-minfee.service" "${DEBIAN_SERVICE_HOME}"
;;
esac
fi
################################
# Bitcoin instance for Testnet #
################################
if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
echo "[*] Installing Bitcoin Testnet service"
case $OS in
FreeBSD)
echo "[*] FIXME: Bitcoin Testnet service must be installed manually on FreeBSD"
;;
Debian)
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/linux/bitcoin-testnet.service" "${DEBIAN_SERVICE_HOME}"
;;
esac
fi
###############################
# Bitcoin instance for Signet #
###############################
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
echo "[*] Installing Bitcoin Signet service"
case $OS in
FreeBSD)
echo "[*] FIXME: Bitcoin Signet service must be installed manually on FreeBSD"
;;
Debian)
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/linux/bitcoin-signet.service" "${DEBIAN_SERVICE_HOME}"
;;
esac
fi
###############################
# Bitcoin instance for Liquid #
###############################
if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
echo "[*] Installing Bitcoin Liquid service"
case $OS in
FreeBSD)
echo "[*] FIXME: Bitcoin Liquid service must be installed manually on FreeBSD"
;;
Debian)
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/linux/liquid.service" "${DEBIAN_SERVICE_HOME}"
;; ;;
esac esac
fi fi
@ -1212,14 +1296,21 @@ if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-mainnet" "${BITCOIN_ELECTRS_HOME}" osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-mainnet" "${BITCOIN_ELECTRS_HOME}"
echo "[*] Installing Bitcoin crontab" echo "[*] Installing Bitcoin crontab"
# FIXME: must only crontab enabled daemons case $OS in
osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin.crontab" FreeBSD)
osSudo "${ROOT_USER}" crontab -u "${MINFEE_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/minfee.crontab" echo [*] FIXME: must only crontab enabled daemons
osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/bitcoin.crontab"
osSudo "${ROOT_USER}" crontab -u "${MINFEE_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/minfee.crontab"
;;
Debian)
(crontab -l ; echo "@reboot sleep 30 ; screen -dmS mainnet /bitcoin/electrs/electrs-start-mainnet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" -
;;
esac
echo "[*] Configuring Bitcoin Mainnet RPC credentials in electrs start script" echo "[*] Configuring Bitcoin Mainnet RPC credentials in electrs start script"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet"
osSudo "${ROOT_USER}" sed -i .orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet" osSudo "${ROOT_USER}" sed -i.orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${BITCOIN_ELECTRS_HOME}/electrs-start-mainnet"
fi fi
######################################## ########################################
@ -1230,10 +1321,17 @@ if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
echo "[*] Installing Bitcoin Testnet electrs start script" echo "[*] Installing Bitcoin Testnet electrs start script"
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-testnet" "${BITCOIN_ELECTRS_HOME}" osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-testnet" "${BITCOIN_ELECTRS_HOME}"
case $OS in
Debian)
echo "[*] Installing Bitcoin-testnet crontab"
(crontab -l ; echo "@reboot sleep 70 ; screen -dmS testnet /bitcoin/electrs/electrs-start-testnet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" -
;;
esac
echo "[*] Configuring Bitcoin Testnet RPC credentials in electrs start script" echo "[*] Configuring Bitcoin Testnet RPC credentials in electrs start script"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet"
osSudo "${ROOT_USER}" sed -i .orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet" osSudo "${ROOT_USER}" sed -i.orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${BITCOIN_ELECTRS_HOME}/electrs-start-testnet"
fi fi
####################################### #######################################
@ -1244,10 +1342,17 @@ if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
echo "[*] Installing Bitcoin Signet electrs start script" echo "[*] Installing Bitcoin Signet electrs start script"
osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-signet" "${BITCOIN_ELECTRS_HOME}" osSudo "${ROOT_USER}" install -c -o "${BITCOIN_USER}" -g "${BITCOIN_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-signet" "${BITCOIN_ELECTRS_HOME}"
case $OS in
Debian)
echo "[*] Installing Bitcoin-signet crontab"
(crontab -l ; echo "@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet") | osSudo "${ROOT_USER}" crontab -u "${BITCOIN_USER}" -
;;
esac
echo "[*] Configuring Bitcoin Signet RPC credentials in electrs start script" echo "[*] Configuring Bitcoin Signet RPC credentials in electrs start script"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet"
osSudo "${ROOT_USER}" sed -i .orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet" osSudo "${ROOT_USER}" sed -i.orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${BITCOIN_ELECTRS_HOME}/electrs-start-signet"
fi fi
######################################## ########################################
@ -1259,13 +1364,20 @@ if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" install -c -o "${ELEMENTS_USER}" -g "${ELEMENTS_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-liquid" "${ELEMENTS_ELECTRS_HOME}" osSudo "${ROOT_USER}" install -c -o "${ELEMENTS_USER}" -g "${ELEMENTS_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-liquid" "${ELEMENTS_ELECTRS_HOME}"
echo "[*] Installing Elements crontab" echo "[*] Installing Elements crontab"
# FIXME: must only crontab enabled daemons case $OS in
osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/elements.crontab" FreeBSD)
echo [*] FIXME: must only crontab enabled daemons
osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/elements.crontab"
;;
Debian)
(crontab -l ; echo "6 * * * * cd $HOME/asset_registry_db && git pull origin master >/dev/null 2>&1") | osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" -
;;
esac
echo "[*] Configuring Elements Liquid RPC credentials in electrs start script" echo "[*] Configuring Elements Liquid RPC credentials in electrs start script"
osSudo "${ROOT_USER}" sed -i .orig "s/__ELEMENTS_RPC_USER__/${ELEMENTS_RPC_USER}/" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquid" osSudo "${ROOT_USER}" sed -i.orig "s/__ELEMENTS_RPC_USER__/${ELEMENTS_RPC_USER}/" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquid"
osSudo "${ROOT_USER}" sed -i .orig "s/__ELEMENTS_RPC_PASS__/${ELEMENTS_RPC_PASS}/" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquid" osSudo "${ROOT_USER}" sed -i.orig "s/__ELEMENTS_RPC_PASS__/${ELEMENTS_RPC_PASS}/" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquid"
osSudo "${ROOT_USER}" sed -i .orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquid" osSudo "${ROOT_USER}" sed -i.orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquid"
fi fi
################################################ ################################################
@ -1276,16 +1388,23 @@ if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then
echo "[*] Installing Elements Liquid Testnet electrs start script" echo "[*] Installing Elements Liquid Testnet electrs start script"
osSudo "${ROOT_USER}" install -c -o "${ELEMENTS_USER}" -g "${ELEMENTS_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-liquidtestnet" "${ELEMENTS_ELECTRS_HOME}" osSudo "${ROOT_USER}" install -c -o "${ELEMENTS_USER}" -g "${ELEMENTS_GROUP}" -m 755 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/electrs-start-liquidtestnet" "${ELEMENTS_ELECTRS_HOME}"
case $OS in
Debian)
echo "[*] Installing Elements-testnet crontab"
(crontab -l ; echo "6 * * * * cd $HOME/asset_registry_testnet_db && git pull origin master >/dev/null 2>&1") | osSudo "${ROOT_USER}" crontab -u "${ELEMENTS_USER}" -
;;
esac
echo "[*] Installing Elements Liquid Testnet RPC credentials" echo "[*] Installing Elements Liquid Testnet RPC credentials"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${ELEMENTS_HOME}/elements.conf" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_USER__/${BITCOIN_RPC_USER}/" "${ELEMENTS_HOME}/elements.conf"
osSudo "${ROOT_USER}" sed -i .orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${ELEMENTS_HOME}/elements.conf" osSudo "${ROOT_USER}" sed -i.orig "s/__BITCOIN_RPC_PASS__/${BITCOIN_RPC_PASS}/" "${ELEMENTS_HOME}/elements.conf"
osSudo "${ROOT_USER}" sed -i .orig "s/__ELEMENTS_RPC_USER__/${ELEMENTS_RPC_USER}/" "${ELEMENTS_HOME}/elements.conf" osSudo "${ROOT_USER}" sed -i.orig "s/__ELEMENTS_RPC_USER__/${ELEMENTS_RPC_USER}/" "${ELEMENTS_HOME}/elements.conf"
osSudo "${ROOT_USER}" sed -i .orig "s/__ELEMENTS_RPC_PASS__/${ELEMENTS_RPC_PASS}/" "${ELEMENTS_HOME}/elements.conf" osSudo "${ROOT_USER}" sed -i.orig "s/__ELEMENTS_RPC_PASS__/${ELEMENTS_RPC_PASS}/" "${ELEMENTS_HOME}/elements.conf"
echo "[*] Configuring Elements LiquidTestnet RPC credentials in electrs start script" echo "[*] Configuring Elements LiquidTestnet RPC credentials in electrs start script"
osSudo "${ROOT_USER}" sed -i .orig "s/__ELEMENTS_RPC_USER__/${ELEMENTS_RPC_USER}/" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquidtestnet" osSudo "${ROOT_USER}" sed -i.orig "s/__ELEMENTS_RPC_USER__/${ELEMENTS_RPC_USER}/" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquidtestnet"
osSudo "${ROOT_USER}" sed -i .orig "s/__ELEMENTS_RPC_PASS__/${ELEMENTS_RPC_PASS}/" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquidtestnet" osSudo "${ROOT_USER}" sed -i.orig "s/__ELEMENTS_RPC_PASS__/${ELEMENTS_RPC_PASS}/" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquidtestnet"
osSudo "${ROOT_USER}" sed -i .orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquidtestnet" osSudo "${ROOT_USER}" sed -i.orig "s!__ELECTRS_DATA_ROOT__!${ELECTRS_DATA_ROOT}!" "${ELEMENTS_ELECTRS_HOME}/electrs-start-liquidtestnet"
fi fi
##################################### #####################################
@ -1394,7 +1513,7 @@ case $OS in
;; ;;
Debian) Debian)
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/nginx.conf" "${NGINX_CONFIGURATION}" osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/nginx/nginx.conf" "${NGINX_CONFIGURATION}"
#echo "[*] Restarting Nginx" #echo "[*] Restarting Nginx"
#osSudo "${ROOT_USER}" service nginx restart #osSudo "${ROOT_USER}" service nginx restart
;; ;;
@ -1415,22 +1534,21 @@ case $OS in
fi fi
if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" systemctl enable bitcoin.service osSudo "${ROOT_USER}" systemctl enable bitcoin.service
osSudo "${ROOT_USER}" systemctl enable electrs.service fi
osSudo "${ROOT_USER}" systemctl enable mempool.service if [ "${BITCOIN_MAINNET_MINFEE_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" systemctl enable bitcoin-minfee.service
fi fi
if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" systemctl enable bitcoin-testnet.service osSudo "${ROOT_USER}" systemctl enable bitcoin-testnet.service
osSudo "${ROOT_USER}" systemctl enable electrs-testnet.service fi
osSudo "${ROOT_USER}" systemctl enable mempool-testnet.service if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" systemctl enable bitcoin-signet.service
fi fi
if [ "${BISQ_MAINNET_ENABLE}" = ON ];then if [ "${BISQ_MAINNET_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" systemctl enable bisq.service osSudo "${ROOT_USER}" systemctl enable bisq.service
osSudo "${ROOT_USER}" systemctl enable mempool-bisq.service
fi fi
if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" systemctl enable liquid.service osSudo "${ROOT_USER}" systemctl enable liquid.service
osSudo "${ROOT_USER}" systemctl enable electrs-liquid.service
osSudo "${ROOT_USER}" systemctl enable mempool-liquid.service
fi fi
;; ;;
esac esac

View File

@ -0,0 +1,22 @@
[Unit]
Description=Bitcoind-minfee
After=network.target
[Service]
ExecStart=/usr/local/bin/bitcoind -daemon -printtoconsole -pid=/minfee/bitcoind-minfee.pid
ExecStop=/usr/local/bin/bitcoin-cli stop
Type=forking
PIDFile=/minfee/bitcoind.pid
Restart=on-failure
User=minfee
Group=minfee
PrivateTmp=true
ProtectSystem=full
NoNewPrivileges=true
PrivateDevices=true
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,22 @@
[Unit]
Description=Bitcoind-signet
After=network.target
[Service]
ExecStart=/usr/local/bin/bitcoind -conf=bitcoin-signet.conf -daemon -signet -printtoconsole -pid=/bitcoin/bitcoind-signet.pid
ExecStop=/usr/local/bin/bitcoin-cli -signet stop
Type=forking
PIDFile=/bitcoin/bitcoind-signet.pid
Restart=on-failure
User=bitcoin
Group=bitcoin
PrivateTmp=true
ProtectSystem=full
NoNewPrivileges=true
PrivateDevices=true
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,22 @@
[Unit]
Description=Bitcoind-testnet
After=network.target
[Service]
ExecStart=/usr/local/bin/bitcoind -conf=bitcoin-testnet.conf -daemon -testnet -printtoconsole -pid=/bitcoin/bitcoind-testnet.pid
ExecStop=/usr/local/bin/bitcoin-cli -testnet stop
Type=forking
PIDFile=/bitcoin/bitcoind-testnet.pid
Restart=on-failure
User=bitcoin
Group=bitcoin
PrivateTmp=true
ProtectSystem=full
NoNewPrivileges=true
PrivateDevices=true
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,22 @@
[Unit]
Description=Bitcoind
After=network.target
[Service]
ExecStart=/usr/local/bin/bitcoind -daemon -printtoconsole -pid=/bitcoin/bitcoind.pid
ExecStop=/usr/local/bin/bitcoin-cli stop
Type=forking
PIDFile=/bitcoin/bitcoind.pid
Restart=on-failure
User=bitcoin
Group=bitcoin
PrivateTmp=true
ProtectSystem=full
NoNewPrivileges=true
PrivateDevices=true
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,22 @@
[Unit]
Description=Liquid
After=network.target
[Service]
ExecStart=/usr/local/bin/elementsd -daemon -printtoconsole -pid=/liquid/liquid.pid
ExecStop=/usr/local/bin/elements-cli stop
Type=forking
PIDFile=/liquid/liquid.pid
Restart=on-failure
User=liquid
Group=liquid
PrivateTmp=true
ProtectSystem=full
NoNewPrivileges=true
PrivateDevices=true
[Install]
WantedBy=multi-user.target

View File

@ -1,4 +1,4 @@
#!/usr/local/bin/zsh #!/usr/bin/env zsh
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:$HOME/bin PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:$HOME/bin
HOSTNAME=$(hostname) HOSTNAME=$(hostname)
LOCATION=$(hostname|cut -d . -f2) LOCATION=$(hostname|cut -d . -f2)
@ -16,10 +16,13 @@ if [ -f "${LOCKFILE}" ];then
exit 1 exit 1
fi fi
trap "rv=\$?; rm -rf "${LOCKFILE}"; exit \$rv" INT TERM EXIT # on exit, remove lockfile but preserve exit code
trap "rv=\$?; rm -f "${LOCKFILE}"; exit \$rv" INT TERM EXIT
# create lockfile
touch "${LOCKFILE}" touch "${LOCKFILE}"
# notify logged in users
echo "Upgrading mempool to ${REF}" | wall echo "Upgrading mempool to ${REF}" | wall
update_repo() update_repo()
@ -84,25 +87,48 @@ ship_frontend()
rsync -av "./dist/mempool/browser/" "${HOME}/public_html/${site}/" || exit 1 rsync -av "./dist/mempool/browser/" "${HOME}/public_html/${site}/" || exit 1
} }
# load nvm if necessary
export NVM_DIR="${HOME}/.nvm" export NVM_DIR="${HOME}/.nvm"
source "${NVM_DIR}/nvm.sh" source "${NVM_DIR}/nvm.sh"
for target in mainnet testnet signet liquid liquidtestnet bisq;do # what to look for
update_repo "${target}" frontends=(mainnet liquid bisq)
backends=(mainnet testnet signet liquid liquidtestnet bisq)
frontend_repos=()
backend_repos=()
# find which frontend repos we have
for repo in $frontends;do
[ -d "${repo}" ] && frontend_repos+="${repo}"
done done
for target in mainnet testnet signet liquid liquidtestnet bisq;do # find which backend repos we have
build_backend "${target}" for repo in $backends;do
[ -d "${repo}" ] && backend_repos+="${repo}"
[ -d "${repo}-lightning" ] && backend_repos+="${repo}-lightning"
done done
for target in mainnet liquid bisq;do # update all repos
build_frontend "${target}" for repo in $backend_repos;do
update_repo "${repo}"
done done
# build backends
for repo in $backend_repos;do
build_backend "${repo}"
done
# build frontends
for repo in $frontend_repos;do
build_frontend "${repo}"
done
# ship frontend dist folders to public_html
for target in mainnet liquid bisq;do for target in mainnet liquid bisq;do
ship_frontend "${target}" ship_frontend "${target}"
done done
# notify everyone
echo "${HOSTNAME} updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general mempool.dev echo "${HOSTNAME} updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general mempool.dev
echo "${HOSTNAME} updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general "mempool.ops.${LOCATION}" echo "${HOSTNAME} updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general "mempool.ops.${LOCATION}"

View File

@ -1,8 +1,8 @@
#!/usr/local/bin/zsh #!/usr/bin/env zsh
export NVM_DIR="$HOME/.nvm" export NVM_DIR="$HOME/.nvm"
source "$NVM_DIR/nvm.sh" source "$NVM_DIR/nvm.sh"
for site in mainnet liquid testnet bisq signet liquidtestnet
do for site in mainnet mainnet-lightning testnet testnet-lightning signet signet-lightning bisq liquid liquidtestnet;do
cd "${HOME}/${site}/backend/" && \ cd "${HOME}/${site}/backend/" && \
screen -dmS "${site}" sh -c 'while true;do npm run start-production;sleep 1;done' screen -dmS "${site}" sh -c 'while true;do npm run start-production;sleep 1;done'
done done

View File

@ -67,6 +67,16 @@ do for url in / \
'/api/v1/mining/blocks/fee-rates/2y' \ '/api/v1/mining/blocks/fee-rates/2y' \
'/api/v1/mining/blocks/fee-rates/3y' \ '/api/v1/mining/blocks/fee-rates/3y' \
'/api/v1/mining/blocks/fee-rates/all' \ '/api/v1/mining/blocks/fee-rates/all' \
'/api/v1/mining/difficulty-adjustments/24h' \
'/api/v1/mining/difficulty-adjustments/3d' \
'/api/v1/mining/difficulty-adjustments/1w' \
'/api/v1/mining/difficulty-adjustments/1m' \
'/api/v1/mining/difficulty-adjustments/3m' \
'/api/v1/mining/difficulty-adjustments/6m' \
'/api/v1/mining/difficulty-adjustments/1y' \
'/api/v1/mining/difficulty-adjustments/2y' \
'/api/v1/mining/difficulty-adjustments/3y' \
'/api/v1/mining/difficulty-adjustments/all' \
do do
curl -s "https://${hostname}${url}" >/dev/null curl -s "https://${hostname}${url}" >/dev/null

View File

@ -4,8 +4,6 @@ tcp_nopush on;
tcp_nodelay on; tcp_nodelay on;
server_tokens off; server_tokens off;
server_name_in_redirect off; server_name_in_redirect off;
include /usr/local/etc/nginx/mime.types;
default_type application/octet-stream;
# default logs # default logs
access_log /var/log/nginx/access.log; access_log /var/log/nginx/access.log;

View File

@ -0,0 +1,20 @@
# route lightning API endpoints to lightning backend
location /api/v1/lightning {
try_files /dev/null @mempool-api-v1-lightning;
}
location @mempool-api-v1-lightning {
proxy_pass $mempoolMainnetLightning;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache api;
proxy_cache_valid 200 10s;
proxy_redirect off;
expires 10s;
}

View File

@ -1,26 +1,51 @@
location /api/v1/statistics { ###########
try_files /dev/null @mempool-api-v1-warmcache; # mempool #
} ###########
location /api/v1/mining {
try_files /dev/null @mempool-api-v1-warmcache; # websocket has special HTTP headers
} location /api/v1/ws {
location /api/v1/block/ { try_files /dev/null @mempool-api-v1-websocket;
try_files /dev/null @mempool-api-v1-forevercache;
}
location /api/v1 {
try_files /dev/null @mempool-api-v1-coldcache;
}
location /api/block/ {
rewrite ^/api/(.*) /$1 break;
try_files /dev/null @electrs-api-forevercache;
}
location /api/ {
rewrite ^/api/(.*) /$1 break;
try_files /dev/null @electrs-api-nocache;
} }
location @mempool-api-v1-forevercache { # warm cache mining and mempool API responses
proxy_pass $mempoolBackend; location /api/v1/statistics {
try_files /dev/null @mempool-api-v1-cache-warm;
}
location /api/v1/mining {
try_files /dev/null @mempool-api-v1-cache-warm;
}
# it's ok to cache blockchain data "forever", so we do 30d
location /api/v1/block/ {
try_files /dev/null @mempool-api-v1-cache-forever;
}
# everything else gets "normal" cache
location /api/v1 {
try_files /dev/null @mempool-api-v1-cache-normal;
}
###########
# esplora #
###########
# it's ok to cache blockchain data "forever", so we do 30d
location /api/block/ {
rewrite ^/api/(.*) /$1 break;
try_files /dev/null @esplora-api-cache-forever;
}
# other API responses cannot be cached
location /api/ {
rewrite ^/api/(.*) /$1 break;
try_files /dev/null @esplora-api-cache-disabled;
}
###########
# routing #
###########
location @mempool-api-v1-websocket {
proxy_pass $mempoolMainnet;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
@ -29,8 +54,16 @@ location @mempool-api-v1-forevercache {
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
}
location @mempool-api-v1-cache-forever {
proxy_pass $mempoolMainnet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_cache_background_update on; proxy_cache_background_update on;
proxy_cache_use_stale updating; proxy_cache_use_stale updating;
proxy_cache api; proxy_cache api;
@ -40,18 +73,14 @@ location @mempool-api-v1-forevercache {
expires 30d; expires 30d;
} }
location @mempool-api-v1-warmcache { location @mempool-api-v1-cache-warm {
proxy_pass $mempoolBackend; proxy_pass $mempoolMainnet;
proxy_http_version 1.1;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_cache_background_update on; proxy_cache_background_update on;
proxy_cache_use_stale updating; proxy_cache_use_stale updating;
proxy_cache api; proxy_cache api;
@ -59,18 +88,14 @@ location @mempool-api-v1-warmcache {
proxy_redirect off; proxy_redirect off;
} }
location @mempool-api-v1-coldcache { location @mempool-api-v1-cache-normal {
proxy_pass $mempoolBackend; proxy_pass $mempoolMainnet;
proxy_http_version 1.1;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_cache api; proxy_cache api;
proxy_cache_valid 200 10s; proxy_cache_valid 200 10s;
proxy_redirect off; proxy_redirect off;
@ -78,54 +103,42 @@ location @mempool-api-v1-coldcache {
expires 10s; expires 10s;
} }
location @mempool-api-v1-nocache { location @mempool-api-v1-cache-disabled {
proxy_pass $mempoolBackend; proxy_pass $mempoolMainnet;
proxy_http_version 1.1;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_redirect off; proxy_redirect off;
proxy_buffering off; proxy_buffering off;
expires -1; expires -1;
} }
location @electrs-api-nocache { location @esplora-api-cache-disabled {
proxy_pass $electrsBackend; proxy_pass $esploraMainnet;
proxy_http_version 1.1;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_redirect off; proxy_redirect off;
proxy_buffering off; proxy_buffering off;
expires -1; expires -1;
} }
location @electrs-api-forevercache { location @esplora-api-cache-forever {
proxy_pass $electrsBackend; proxy_pass $esploraMainnet;
proxy_http_version 1.1;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_cache_background_update on; proxy_cache_background_update on;
proxy_cache_use_stale updating; proxy_cache_use_stale updating;
proxy_cache api; proxy_cache api;

View File

@ -1,12 +1,150 @@
###########
# mempool #
###########
# websocket has special HTTP headers
location /liquid/api/v1/ws { location /liquid/api/v1/ws {
proxy_pass http://mempool-liquid-mainnet/; rewrite ^/liquid/(.*) /$1 break;
proxy_http_version 1.1; try_files /dev/null @mempool-liquid-api-v1-websocket;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
} }
# warm cache mempool API responses
location /liquid/api/v1/statistics {
rewrite ^/liquid/(.*) /$1 break;
try_files /dev/null @mempool-liquid-api-v1-cache-warm;
}
# it's ok to cache blockchain data "forever", so we do 30d
location /liquid/api/v1/block/ {
rewrite ^/liquid/(.*) /$1 break;
try_files /dev/null @mempool-liquid-api-v1-cache-forever;
}
# everything else gets "normal" cache
location /liquid/api/v1 { location /liquid/api/v1 {
proxy_pass http://mempool-liquid-mainnet/api/v1; rewrite ^/liquid/(.*) /$1 break;
try_files /dev/null @mempool-liquid-api-v1-cache-normal;
} }
###########
# esplora #
###########
# it's ok to cache blockchain data "forever", so we do 30d
location /liquid/api/block/ {
rewrite ^/liquid/api/(.*) /$1 break;
try_files /dev/null @esplora-liquid-api-cache-forever;
}
# other API responses cannot be cached
location /liquid/api/ { location /liquid/api/ {
proxy_pass http://electrs-liquid-mainnet/; rewrite ^/liquid/api/(.*) /$1 break;
try_files /dev/null @esplora-liquid-api-cache-disabled;
}
###########
# routing #
###########
location @mempool-liquid-api-v1-websocket {
proxy_pass $mempoolMainnet;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
}
location @mempool-liquid-api-v1-cache-forever {
proxy_pass $mempoolMainnet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache api;
proxy_cache_valid 200 30d;
proxy_redirect off;
expires 30d;
}
location @mempool-liquid-api-v1-cache-warm {
proxy_pass $mempoolMainnet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache api;
proxy_cache_valid 200 10s;
proxy_redirect off;
}
location @mempool-liquid-api-v1-cache-normal {
proxy_pass $mempoolMainnet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache api;
proxy_cache_valid 200 10s;
proxy_redirect off;
expires 10s;
}
location @mempool-liquid-api-v1-cache-disabled {
proxy_pass $mempoolMainnet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
expires -1;
}
location @esplora-liquid-api-cache-disabled {
proxy_pass $esploraMainnet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
expires -1;
}
location @esplora-liquid-api-cache-forever {
proxy_pass $esploraMainnet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache api;
proxy_cache_valid 200 30d;
proxy_redirect off;
expires 30d;
} }

View File

@ -1,12 +1,154 @@
###########
# mempool #
###########
# websocket has special HTTP headers
location /liquidtestnet/api/v1/ws { location /liquidtestnet/api/v1/ws {
proxy_pass http://mempool-liquid-testnet/; rewrite ^/liquidtestnet/(.*) /$1 break;
proxy_http_version 1.1; try_files /dev/null @mempool-liquidtestnet-api-v1-websocket;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
} }
# warm cache mining and mempool API responses
location /liquidtestnet/api/v1/statistics {
rewrite ^/liquidtestnet/(.*) /$1 break;
try_files /dev/null @mempool-liquidtestnet-api-v1-cache-warm;
}
location /liquidtestnet/api/v1/mining {
rewrite ^/liquidtestnet/(.*) /$1 break;
try_files /dev/null @mempool-liquidtestnet-api-v1-cache-warm;
}
# it's ok to cache blockchain data "forever", so we do 30d
location /liquidtestnet/api/v1/block/ {
rewrite ^/liquidtestnet/(.*) /$1 break;
try_files /dev/null @mempool-liquidtestnet-api-v1-cache-forever;
}
# everything else gets "normal" cache
location /liquidtestnet/api/v1 { location /liquidtestnet/api/v1 {
proxy_pass http://mempool-liquid-testnet/api/v1; rewrite ^/liquidtestnet/(.*) /$1 break;
try_files /dev/null @mempool-liquidtestnet-api-v1-cache-normal;
} }
###########
# esplora #
###########
# it's ok to cache blockchain data "forever", so we do 30d
location /liquidtestnet/api/block/ {
rewrite ^/liquidtestnet/api/(.*) /$1 break;
try_files /dev/null @esplora-liquidtestnet-api-cache-forever;
}
# other API responses cannot be cached
location /liquidtestnet/api/ { location /liquidtestnet/api/ {
proxy_pass http://electrs-liquid-testnet/; rewrite ^/liquidtestnet/api/(.*) /$1 break;
try_files /dev/null @esplora-liquidtestnet-api-cache-disabled;
}
###########
# routing #
###########
location @mempool-liquidtestnet-api-v1-websocket {
proxy_pass $mempoolTestnet;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
}
location @mempool-liquidtestnet-api-v1-cache-forever {
proxy_pass $mempoolTestnet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache api;
proxy_cache_valid 200 30d;
proxy_redirect off;
expires 30d;
}
location @mempool-liquidtestnet-api-v1-cache-warm {
proxy_pass $mempoolTestnet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache api;
proxy_cache_valid 200 10s;
proxy_redirect off;
}
location @mempool-liquidtestnet-api-v1-cache-normal {
proxy_pass $mempoolTestnet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache api;
proxy_cache_valid 200 10s;
proxy_redirect off;
expires 10s;
}
location @mempool-liquidtestnet-api-v1-cache-disabled {
proxy_pass $mempoolTestnet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
expires -1;
}
location @esplora-liquidtestnet-api-cache-disabled {
proxy_pass $esploraTestnet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
expires -1;
}
location @esplora-liquidtestnet-api-cache-forever {
proxy_pass $esploraTestnet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache api;
proxy_cache_valid 200 30d;
proxy_redirect off;
expires 30d;
} }

View File

@ -0,0 +1,21 @@
# route lightning API endpoints to lightning backend
location /signet/api/v1/lightning {
rewrite ^/signet/(.*) /$1 break;
try_files /dev/null @mempool-signet-api-v1-lightning;
}
location @mempool-signet-api-v1-lightning {
proxy_pass $mempoolSignetLightning;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache api;
proxy_cache_valid 200 10s;
proxy_redirect off;
expires 10s;
}

View File

@ -1,12 +1,154 @@
###########
# mempool #
###########
# websocket has special HTTP headers
location /signet/api/v1/ws { location /signet/api/v1/ws {
proxy_pass http://mempool-bitcoin-signet/; rewrite ^/signet/(.*) /$1 break;
proxy_http_version 1.1; try_files /dev/null @mempool-signet-api-v1-websocket;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
} }
# warm cache mining and mempool API responses
location /signet/api/v1/statistics {
rewrite ^/signet/(.*) /$1 break;
try_files /dev/null @mempool-signet-api-v1-cache-warm;
}
location /signet/api/v1/mining {
rewrite ^/signet/(.*) /$1 break;
try_files /dev/null @mempool-signet-api-v1-cache-warm;
}
# it's ok to cache blockchain data "forever", so we do 30d
location /signet/api/v1/block/ {
rewrite ^/signet/(.*) /$1 break;
try_files /dev/null @mempool-signet-api-v1-cache-forever;
}
# everything else gets "normal" cache
location /signet/api/v1 { location /signet/api/v1 {
proxy_pass http://mempool-bitcoin-signet/api/v1; rewrite ^/signet/(.*) /$1 break;
try_files /dev/null @mempool-signet-api-v1-cache-normal;
} }
###########
# esplora #
###########
# it's ok to cache blockchain data "forever", so we do 30d
location /signet/api/block/ {
rewrite ^/signet/api/(.*) /$1 break;
try_files /dev/null @esplora-signet-api-cache-forever;
}
# other API responses cannot be cached
location /signet/api/ { location /signet/api/ {
proxy_pass http://electrs-bitcoin-signet/; rewrite ^/signet/api/(.*) /$1 break;
try_files /dev/null @esplora-signet-api-cache-disabled;
}
###########
# routing #
###########
location @mempool-signet-api-v1-websocket {
proxy_pass $mempoolSignet;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
}
location @mempool-signet-api-v1-cache-forever {
proxy_pass $mempoolSignet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache api;
proxy_cache_valid 200 30d;
proxy_redirect off;
expires 30d;
}
location @mempool-signet-api-v1-cache-warm {
proxy_pass $mempoolSignet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache api;
proxy_cache_valid 200 10s;
proxy_redirect off;
}
location @mempool-signet-api-v1-cache-normal {
proxy_pass $mempoolSignet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache api;
proxy_cache_valid 200 10s;
proxy_redirect off;
expires 10s;
}
location @mempool-signet-api-v1-cache-disabled {
proxy_pass $mempoolSignet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
expires -1;
}
location @esplora-signet-api-cache-disabled {
proxy_pass $esploraSignet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
expires -1;
}
location @esplora-signet-api-cache-forever {
proxy_pass $esploraSignet;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache api;
proxy_cache_valid 200 30d;
proxy_redirect off;
expires 30d;
} }

View File

@ -0,0 +1,21 @@
# route lightning API endpoints to lightning backend
location /testnet/api/v1/lightning {
rewrite ^/testnet/(.*) /$1 break;
try_files /dev/null @mempool-testnet-api-v1-lightning;
}
location @mempool-testnet-api-v1-lightning {
proxy_pass $mempoolSignetLightning;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_background_update on;
proxy_cache_use_stale updating;
proxy_cache api;
proxy_cache_valid 200 10s;
proxy_redirect off;
expires 10s;
}

Some files were not shown because too many files have changed in this diff Show More