Compare commits
16 Commits
mononaut/f
...
mononaut/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70e19bc41c | ||
|
|
95df317f56 | ||
|
|
f61f520a4b | ||
|
|
864225a0dc | ||
|
|
5628da2f80 | ||
|
|
000c46bf57 | ||
|
|
66919a1aba | ||
|
|
2fbe2b2fa6 | ||
|
|
d293d637b5 | ||
|
|
04a8249883 | ||
|
|
a196c276c9 | ||
|
|
dfe2cf631f | ||
|
|
d6913b6439 | ||
|
|
32cd8bb3cb | ||
|
|
5950034f53 | ||
|
|
d325734c16 |
@@ -44,7 +44,8 @@
|
|||||||
},
|
},
|
||||||
"ESPLORA": {
|
"ESPLORA": {
|
||||||
"REST_API_URL": "http://127.0.0.1:3000",
|
"REST_API_URL": "http://127.0.0.1:3000",
|
||||||
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet"
|
"UNIX_SOCKET_PATH": "/tmp/esplora-bitcoin-mainnet",
|
||||||
|
"RETRY_UNIX_SOCKET_AFTER": 30000
|
||||||
},
|
},
|
||||||
"SECOND_CORE_RPC": {
|
"SECOND_CORE_RPC": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
|
|||||||
@@ -45,7 +45,8 @@
|
|||||||
},
|
},
|
||||||
"ESPLORA": {
|
"ESPLORA": {
|
||||||
"REST_API_URL": "__ESPLORA_REST_API_URL__",
|
"REST_API_URL": "__ESPLORA_REST_API_URL__",
|
||||||
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__"
|
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
|
||||||
|
"RETRY_UNIX_SOCKET_AFTER": "__ESPLORA_RETRY_UNIX_SOCKET_AFTER__"
|
||||||
},
|
},
|
||||||
"SECOND_CORE_RPC": {
|
"SECOND_CORE_RPC": {
|
||||||
"HOST": "__SECOND_CORE_RPC_HOST__",
|
"HOST": "__SECOND_CORE_RPC_HOST__",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ describe('Mempool Backend Config', () => {
|
|||||||
|
|
||||||
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
|
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });
|
||||||
|
|
||||||
expect(config.ESPLORA).toStrictEqual({ REST_API_URL: 'http://127.0.0.1:3000', UNIX_SOCKET_PATH: null });
|
expect(config.ESPLORA).toStrictEqual({ REST_API_URL: 'http://127.0.0.1:3000', UNIX_SOCKET_PATH: null, RETRY_UNIX_SOCKET_AFTER: 30000 });
|
||||||
|
|
||||||
expect(config.CORE_RPC).toStrictEqual({
|
expect(config.CORE_RPC).toStrictEqual({
|
||||||
HOST: '127.0.0.1',
|
HOST: '127.0.0.1',
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class BitcoinRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
||||||
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
|
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
|
||||||
@@ -110,7 +111,6 @@ class BitcoinRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/raw', this.getRawBlock)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/raw', this.getRawBlock)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', this.getTxIdsForBlock)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', this.getTxIdsForBlock)
|
||||||
@@ -589,10 +589,14 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getBlockTipHeight(req: Request, res: Response) {
|
private getBlockTipHeight(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const result = await bitcoinApi.$getBlockHeightTip();
|
const result = blocks.getCurrentBlockHeight();
|
||||||
res.json(result);
|
if (!result) {
|
||||||
|
return res.status(503).send(`Service Temporarily Unavailable`);
|
||||||
|
}
|
||||||
|
res.setHeader('content-type', 'text/plain');
|
||||||
|
res.send(result.toString());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,102 +3,68 @@ import axios, { AxiosRequestConfig } from 'axios';
|
|||||||
import http from 'http';
|
import http from 'http';
|
||||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||||
import { IEsploraApi } from './esplora-api.interface';
|
import { IEsploraApi } from './esplora-api.interface';
|
||||||
import logger from '../../logger';
|
|
||||||
|
|
||||||
const axiosConnection = axios.create({
|
const axiosConnection = axios.create({
|
||||||
httpAgent: new http.Agent({ keepAlive: true, })
|
httpAgent: new http.Agent({ keepAlive: true, })
|
||||||
});
|
});
|
||||||
|
|
||||||
class ElectrsApi implements AbstractBitcoinApi {
|
class ElectrsApi implements AbstractBitcoinApi {
|
||||||
private axiosConfigWithUnixSocket: AxiosRequestConfig = config.ESPLORA.UNIX_SOCKET_PATH ? {
|
axiosConfig: AxiosRequestConfig = config.ESPLORA.UNIX_SOCKET_PATH ? {
|
||||||
socketPath: config.ESPLORA.UNIX_SOCKET_PATH,
|
socketPath: config.ESPLORA.UNIX_SOCKET_PATH,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
} : {
|
} : {
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
};
|
};
|
||||||
private axiosConfigTcpSocketOnly: AxiosRequestConfig = {
|
|
||||||
timeout: 10000,
|
|
||||||
};
|
|
||||||
|
|
||||||
unixSocketRetryTimeout;
|
constructor() { }
|
||||||
activeAxiosConfig;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
|
|
||||||
}
|
|
||||||
|
|
||||||
fallbackToTcpSocket() {
|
|
||||||
if (!this.unixSocketRetryTimeout) {
|
|
||||||
logger.err(`Unable to connect to esplora unix socket. Falling back to tcp socket. Retrying unix socket in ${config.ESPLORA.RETRY_UNIX_SOCKET_AFTER / 1000} seconds`);
|
|
||||||
// Retry the unix socket after a few seconds
|
|
||||||
this.unixSocketRetryTimeout = setTimeout(() => {
|
|
||||||
logger.info(`Retrying to use unix socket for esplora now (applied for the next query)`);
|
|
||||||
this.activeAxiosConfig = this.axiosConfigWithUnixSocket;
|
|
||||||
this.unixSocketRetryTimeout = undefined;
|
|
||||||
}, config.ESPLORA.RETRY_UNIX_SOCKET_AFTER);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the TCP socket (reach a different esplora instance through nginx)
|
|
||||||
this.activeAxiosConfig = this.axiosConfigTcpSocketOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
$queryWrapper<T>(url, responseType = 'json'): Promise<T> {
|
|
||||||
return axiosConnection.get<T>(url, { ...this.activeAxiosConfig, responseType: responseType })
|
|
||||||
.then((response) => response.data)
|
|
||||||
.catch((e) => {
|
|
||||||
if (e?.code === 'ECONNREFUSED') {
|
|
||||||
this.fallbackToTcpSocket();
|
|
||||||
// Retry immediately
|
|
||||||
return axiosConnection.get<T>(url, this.activeAxiosConfig)
|
|
||||||
.then((response) => response.data)
|
|
||||||
.catch((e) => {
|
|
||||||
logger.warn(`Cannot query esplora through the unix socket nor the tcp socket. Exception ${e}`);
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
|
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
|
||||||
return this.$queryWrapper<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids');
|
return axiosConnection.get<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids', this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
|
$getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
|
||||||
return this.$queryWrapper<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId);
|
return axiosConnection.get<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getTransactionHex(txId: string): Promise<string> {
|
$getTransactionHex(txId: string): Promise<string> {
|
||||||
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex');
|
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex', this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getBlockHeightTip(): Promise<number> {
|
$getBlockHeightTip(): Promise<number> {
|
||||||
return this.$queryWrapper<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height');
|
return axiosConnection.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getBlockHashTip(): Promise<string> {
|
$getBlockHashTip(): Promise<string> {
|
||||||
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash');
|
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/blocks/tip/hash', this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
||||||
return this.$queryWrapper<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids');
|
return axiosConnection.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getBlockHash(height: number): Promise<string> {
|
$getBlockHash(height: number): Promise<string> {
|
||||||
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height);
|
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height, this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getBlockHeader(hash: string): Promise<string> {
|
$getBlockHeader(hash: string): Promise<string> {
|
||||||
return this.$queryWrapper<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header');
|
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header', this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getBlock(hash: string): Promise<IEsploraApi.Block> {
|
$getBlock(hash: string): Promise<IEsploraApi.Block> {
|
||||||
return this.$queryWrapper<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash);
|
return axiosConnection.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getRawBlock(hash: string): Promise<Buffer> {
|
$getRawBlock(hash: string): Promise<Buffer> {
|
||||||
return this.$queryWrapper<any>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", 'arraybuffer')
|
return axiosConnection.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", { ...this.axiosConfig, responseType: 'arraybuffer' })
|
||||||
.then((response) => { return Buffer.from(response.data); });
|
.then((response) => { return Buffer.from(response.data); });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,11 +85,13 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
|
||||||
return this.$queryWrapper<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout);
|
return axiosConnection.get<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
|
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
|
||||||
return this.$queryWrapper<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends');
|
return axiosConnection.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig)
|
||||||
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
|
async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class Blocks {
|
|||||||
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
||||||
private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => Promise<void>)[] = [];
|
private newAsyncBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => Promise<void>)[] = [];
|
||||||
|
|
||||||
|
private mainLoopTimeout: number = 120000;
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
public getBlocks(): BlockExtended[] {
|
public getBlocks(): BlockExtended[] {
|
||||||
@@ -528,8 +530,12 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $updateBlocks() {
|
public async $updateBlocks() {
|
||||||
|
// throw an error if this stalls the main loop for more than 2 minutes
|
||||||
|
const timer = this.startTimer();
|
||||||
|
|
||||||
let fastForwarded = false;
|
let fastForwarded = false;
|
||||||
const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
|
const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
|
||||||
|
this.updateTimerProgress(timer, 'got block height tip');
|
||||||
|
|
||||||
if (this.blocks.length === 0) {
|
if (this.blocks.length === 0) {
|
||||||
this.currentBlockHeight = Math.max(blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT, -1);
|
this.currentBlockHeight = Math.max(blockHeightTip - config.MEMPOOL.INITIAL_BLOCKS_AMOUNT, -1);
|
||||||
@@ -547,16 +553,21 @@ class Blocks {
|
|||||||
|
|
||||||
if (!this.lastDifficultyAdjustmentTime) {
|
if (!this.lastDifficultyAdjustmentTime) {
|
||||||
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||||
|
this.updateTimerProgress(timer, 'got blockchain info for initial difficulty adjustment');
|
||||||
if (blockchainInfo.blocks === blockchainInfo.headers) {
|
if (blockchainInfo.blocks === blockchainInfo.headers) {
|
||||||
const heightDiff = blockHeightTip % 2016;
|
const heightDiff = blockHeightTip % 2016;
|
||||||
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
|
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
|
||||||
|
this.updateTimerProgress(timer, 'got block hash for initial difficulty adjustment');
|
||||||
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
|
const block: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(blockHash);
|
||||||
|
this.updateTimerProgress(timer, 'got block for initial difficulty adjustment');
|
||||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||||
this.currentDifficulty = block.difficulty;
|
this.currentDifficulty = block.difficulty;
|
||||||
|
|
||||||
if (blockHeightTip >= 2016) {
|
if (blockHeightTip >= 2016) {
|
||||||
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
|
const previousPeriodBlockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff - 2016);
|
||||||
|
this.updateTimerProgress(timer, 'got previous block hash for initial difficulty adjustment');
|
||||||
const previousPeriodBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(previousPeriodBlockHash);
|
const previousPeriodBlock: IEsploraApi.Block = await bitcoinCoreApi.$getBlock(previousPeriodBlockHash);
|
||||||
|
this.updateTimerProgress(timer, 'got previous block for initial difficulty adjustment');
|
||||||
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
|
this.previousDifficultyRetarget = (block.difficulty - previousPeriodBlock.difficulty) / previousPeriodBlock.difficulty * 100;
|
||||||
logger.debug(`Initial difficulty adjustment data set.`);
|
logger.debug(`Initial difficulty adjustment data set.`);
|
||||||
}
|
}
|
||||||
@@ -571,9 +582,11 @@ class Blocks {
|
|||||||
} else {
|
} else {
|
||||||
this.currentBlockHeight++;
|
this.currentBlockHeight++;
|
||||||
logger.debug(`New block found (#${this.currentBlockHeight})!`);
|
logger.debug(`New block found (#${this.currentBlockHeight})!`);
|
||||||
|
this.updateTimerProgress(timer, `getting orphaned blocks for ${this.currentBlockHeight}`);
|
||||||
await chainTips.updateOrphanedBlocks();
|
await chainTips.updateOrphanedBlocks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.updateTimerProgress(timer, `getting block data for ${this.currentBlockHeight}`);
|
||||||
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
|
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
|
||||||
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
|
const verboseBlock = await bitcoinClient.getBlock(blockHash, 2);
|
||||||
const block = BitcoinApi.convertBlock(verboseBlock);
|
const block = BitcoinApi.convertBlock(verboseBlock);
|
||||||
@@ -582,39 +595,51 @@ class Blocks {
|
|||||||
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
|
const cpfpSummary: CpfpSummary = Common.calculateCpfp(block.height, transactions);
|
||||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||||
const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
|
const blockSummary: BlockSummary = this.summarizeBlock(verboseBlock);
|
||||||
|
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
|
||||||
|
|
||||||
// start async callbacks
|
// start async callbacks
|
||||||
|
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
|
||||||
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
|
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
|
||||||
|
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled()) {
|
||||||
if (!fastForwarded) {
|
if (!fastForwarded) {
|
||||||
const lastBlock = await blocksRepository.$getBlockByHeight(blockExtended.height - 1);
|
const lastBlock = await blocksRepository.$getBlockByHeight(blockExtended.height - 1);
|
||||||
|
this.updateTimerProgress(timer, `got block by height for ${this.currentBlockHeight}`);
|
||||||
if (lastBlock !== null && blockExtended.previousblockhash !== lastBlock.id) {
|
if (lastBlock !== null && blockExtended.previousblockhash !== lastBlock.id) {
|
||||||
logger.warn(`Chain divergence detected at block ${lastBlock.height}, re-indexing most recent data`, logger.tags.mining);
|
logger.warn(`Chain divergence detected at block ${lastBlock.height}, re-indexing most recent data`, logger.tags.mining);
|
||||||
// We assume there won't be a reorg with more than 10 block depth
|
// We assume there won't be a reorg with more than 10 block depth
|
||||||
|
this.updateTimerProgress(timer, `rolling back diverged chain from ${this.currentBlockHeight}`);
|
||||||
await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10);
|
await BlocksRepository.$deleteBlocksFrom(lastBlock.height - 10);
|
||||||
await HashratesRepository.$deleteLastEntries();
|
await HashratesRepository.$deleteLastEntries();
|
||||||
await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10);
|
await cpfpRepository.$deleteClustersFrom(lastBlock.height - 10);
|
||||||
|
this.updateTimerProgress(timer, `rolled back chain divergence from ${this.currentBlockHeight}`);
|
||||||
for (let i = 10; i >= 0; --i) {
|
for (let i = 10; i >= 0; --i) {
|
||||||
const newBlock = await this.$indexBlock(lastBlock.height - i);
|
const newBlock = await this.$indexBlock(lastBlock.height - i);
|
||||||
|
this.updateTimerProgress(timer, `reindexed block`);
|
||||||
await this.$getStrippedBlockTransactions(newBlock.id, true, true);
|
await this.$getStrippedBlockTransactions(newBlock.id, true, true);
|
||||||
|
this.updateTimerProgress(timer, `reindexed block summary`);
|
||||||
if (config.MEMPOOL.CPFP_INDEXING) {
|
if (config.MEMPOOL.CPFP_INDEXING) {
|
||||||
await this.$indexCPFP(newBlock.id, lastBlock.height - i);
|
await this.$indexCPFP(newBlock.id, lastBlock.height - i);
|
||||||
|
this.updateTimerProgress(timer, `reindexed block cpfp`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await mining.$indexDifficultyAdjustments();
|
await mining.$indexDifficultyAdjustments();
|
||||||
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
|
await DifficultyAdjustmentsRepository.$deleteLastAdjustment();
|
||||||
|
this.updateTimerProgress(timer, `reindexed difficulty adjustments`);
|
||||||
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`, logger.tags.mining);
|
logger.info(`Re-indexed 10 blocks and summaries. Also re-indexed the last difficulty adjustments. Will re-index latest hashrates in a few seconds.`, logger.tags.mining);
|
||||||
indexer.reindex();
|
indexer.reindex();
|
||||||
}
|
}
|
||||||
await blocksRepository.$saveBlockInDatabase(blockExtended);
|
await blocksRepository.$saveBlockInDatabase(blockExtended);
|
||||||
|
this.updateTimerProgress(timer, `saved ${this.currentBlockHeight} to database`);
|
||||||
|
|
||||||
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
const lastestPriceId = await PricesRepository.$getLatestPriceId();
|
||||||
|
this.updateTimerProgress(timer, `got latest price id ${this.currentBlockHeight}`);
|
||||||
if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
|
if (priceUpdater.historyInserted === true && lastestPriceId !== null) {
|
||||||
await blocksRepository.$saveBlockPrices([{
|
await blocksRepository.$saveBlockPrices([{
|
||||||
height: blockExtended.height,
|
height: blockExtended.height,
|
||||||
priceId: lastestPriceId,
|
priceId: lastestPriceId,
|
||||||
}]);
|
}]);
|
||||||
|
this.updateTimerProgress(timer, `saved prices for ${this.currentBlockHeight}`);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining);
|
logger.debug(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -625,9 +650,11 @@ class Blocks {
|
|||||||
// Save blocks summary for visualization if it's enabled
|
// Save blocks summary for visualization if it's enabled
|
||||||
if (Common.blocksSummariesIndexingEnabled() === true) {
|
if (Common.blocksSummariesIndexingEnabled() === true) {
|
||||||
await this.$getStrippedBlockTransactions(blockExtended.id, true);
|
await this.$getStrippedBlockTransactions(blockExtended.id, true);
|
||||||
|
this.updateTimerProgress(timer, `saved block summary for ${this.currentBlockHeight}`);
|
||||||
}
|
}
|
||||||
if (config.MEMPOOL.CPFP_INDEXING) {
|
if (config.MEMPOOL.CPFP_INDEXING) {
|
||||||
this.$saveCpfp(blockExtended.id, this.currentBlockHeight, cpfpSummary);
|
this.$saveCpfp(blockExtended.id, this.currentBlockHeight, cpfpSummary);
|
||||||
|
this.updateTimerProgress(timer, `saved cpfp for ${this.currentBlockHeight}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -640,6 +667,7 @@ class Blocks {
|
|||||||
difficulty: block.difficulty,
|
difficulty: block.difficulty,
|
||||||
adjustment: Math.round((block.difficulty / this.currentDifficulty) * 1000000) / 1000000, // Remove float point noise
|
adjustment: Math.round((block.difficulty / this.currentDifficulty) * 1000000) / 1000000, // Remove float point noise
|
||||||
});
|
});
|
||||||
|
this.updateTimerProgress(timer, `saved difficulty adjustment for ${this.currentBlockHeight}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
|
this.previousDifficultyRetarget = (block.difficulty - this.currentDifficulty) / this.currentDifficulty * 100;
|
||||||
@@ -664,7 +692,38 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// wait for pending async callbacks to finish
|
// wait for pending async callbacks to finish
|
||||||
|
this.updateTimerProgress(timer, `waiting for async callbacks to complete for ${this.currentBlockHeight}`);
|
||||||
await Promise.all(callbackPromises);
|
await Promise.all(callbackPromises);
|
||||||
|
this.updateTimerProgress(timer, `async callbacks completed for ${this.currentBlockHeight}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clearTimer(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private startTimer() {
|
||||||
|
const state: any = {
|
||||||
|
start: Date.now(),
|
||||||
|
progress: 'begin $updateBlocks',
|
||||||
|
timer: null,
|
||||||
|
};
|
||||||
|
state.timer = setTimeout(() => {
|
||||||
|
logger.err(`$updateBlocks stalled at "${state.progress}`);
|
||||||
|
}, this.mainLoopTimeout);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateTimerProgress(state, msg) {
|
||||||
|
state.progress = msg;
|
||||||
|
if (Date.now() - state.start > this.mainLoopTimeout) {
|
||||||
|
// abort the function if it already timed out
|
||||||
|
logger.err(`$updateBlocks attempted to resume after "${state.progress}"`);
|
||||||
|
throw new Error(`$updateBlocks attempted to resume after "${state.progress}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearTimer(state) {
|
||||||
|
if (state.timer) {
|
||||||
|
clearTimeout(state.timer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class Mempool {
|
|||||||
private timer = new Date().getTime();
|
private timer = new Date().getTime();
|
||||||
private missingTxCount = 0;
|
private missingTxCount = 0;
|
||||||
|
|
||||||
|
private mainLoopTimeout: number = 120000;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
setInterval(this.updateTxPerSecond.bind(this), 1000);
|
setInterval(this.updateTxPerSecond.bind(this), 1000);
|
||||||
}
|
}
|
||||||
@@ -119,10 +121,15 @@ class Mempool {
|
|||||||
|
|
||||||
public async $updateMempool(): Promise<void> {
|
public async $updateMempool(): Promise<void> {
|
||||||
logger.debug(`Updating mempool...`);
|
logger.debug(`Updating mempool...`);
|
||||||
|
|
||||||
|
// throw an error if this stalls the main loop for more than 2 minutes
|
||||||
|
const timer = this.startTimer();
|
||||||
|
|
||||||
const start = new Date().getTime();
|
const start = new Date().getTime();
|
||||||
let hasChange: boolean = false;
|
let hasChange: boolean = false;
|
||||||
const currentMempoolSize = Object.keys(this.mempoolCache).length;
|
const currentMempoolSize = Object.keys(this.mempoolCache).length;
|
||||||
const transactions = await bitcoinApi.$getRawMempool();
|
const transactions = await bitcoinApi.$getRawMempool();
|
||||||
|
this.updateTimerProgress(timer, 'got raw mempool');
|
||||||
const diff = transactions.length - currentMempoolSize;
|
const diff = transactions.length - currentMempoolSize;
|
||||||
const newTransactions: TransactionExtended[] = [];
|
const newTransactions: TransactionExtended[] = [];
|
||||||
|
|
||||||
@@ -146,6 +153,7 @@ class Mempool {
|
|||||||
if (!this.mempoolCache[txid]) {
|
if (!this.mempoolCache[txid]) {
|
||||||
try {
|
try {
|
||||||
const transaction = await transactionUtils.$getTransactionExtended(txid);
|
const transaction = await transactionUtils.$getTransactionExtended(txid);
|
||||||
|
this.updateTimerProgress(timer, 'fetched new transaction');
|
||||||
this.mempoolCache[txid] = transaction;
|
this.mempoolCache[txid] = transaction;
|
||||||
if (this.inSync) {
|
if (this.inSync) {
|
||||||
this.txPerSecondArray.push(new Date().getTime());
|
this.txPerSecondArray.push(new Date().getTime());
|
||||||
@@ -223,12 +231,43 @@ class Mempool {
|
|||||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
||||||
}
|
}
|
||||||
if (this.asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
if (this.asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||||
|
this.updateTimerProgress(timer, 'running async mempool callback');
|
||||||
await this.asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
await this.asyncMempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
||||||
|
this.updateTimerProgress(timer, 'completed async mempool callback');
|
||||||
}
|
}
|
||||||
|
|
||||||
const end = new Date().getTime();
|
const end = new Date().getTime();
|
||||||
const time = end - start;
|
const time = end - start;
|
||||||
logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);
|
logger.debug(`Mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);
|
||||||
|
|
||||||
|
this.clearTimer(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private startTimer() {
|
||||||
|
const state: any = {
|
||||||
|
start: Date.now(),
|
||||||
|
progress: 'begin $updateMempool',
|
||||||
|
timer: null,
|
||||||
|
};
|
||||||
|
state.timer = setTimeout(() => {
|
||||||
|
logger.err(`$updateMempool stalled at "${state.progress}`);
|
||||||
|
}, this.mainLoopTimeout);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateTimerProgress(state, msg) {
|
||||||
|
state.progress = msg;
|
||||||
|
if (Date.now() - state.start > this.mainLoopTimeout) {
|
||||||
|
// abort the function if it already timed out
|
||||||
|
logger.err(`$updateMempool attempted to resume after "${state.progress}"`);
|
||||||
|
throw new Error(`$updateMempool attempted to resume after "${state.progress}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearTimer(state) {
|
||||||
|
if (state.timer) {
|
||||||
|
clearTimeout(state.timer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }) {
|
public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }) {
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
|
|||||||
|
|
||||||
if (nextTx && !nextTx?.used) {
|
if (nextTx && !nextTx?.used) {
|
||||||
// Check if the package fits into this block
|
// Check if the package fits into this block
|
||||||
if (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS) {
|
if (blocks.length >= 7 || (blockWeight + nextTx.ancestorWeight < config.MEMPOOL.BLOCK_WEIGHT_UNITS)) {
|
||||||
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
|
const ancestors: AuditTransaction[] = Array.from(nextTx.ancestorMap.values());
|
||||||
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
// sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
|
||||||
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
|
||||||
@@ -175,34 +175,14 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
|
|||||||
overflow = [];
|
overflow = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// pack any leftover transactions into the last block
|
|
||||||
for (const tx of overflow) {
|
if (overflow.length > 0) {
|
||||||
if (!tx || tx?.used) {
|
logger.warn('GBT overflow list unexpectedly non-empty after final block constructed');
|
||||||
continue;
|
|
||||||
}
|
|
||||||
blockWeight += tx.weight;
|
|
||||||
const mempoolTx = mempool[tx.txid];
|
|
||||||
// update original copy of this tx with effective fee rate & relatives data
|
|
||||||
mempoolTx.effectiveFeePerVsize = tx.score;
|
|
||||||
if (tx.ancestorMap.size > 0) {
|
|
||||||
cpfpClusters[tx.txid] = Array.from(tx.ancestorMap?.values()).map(a => a.txid);
|
|
||||||
mempoolTx.cpfpRoot = tx.txid;
|
|
||||||
}
|
|
||||||
mempoolTx.cpfpChecked = true;
|
|
||||||
transactions.push(tx);
|
|
||||||
tx.used = true;
|
|
||||||
}
|
}
|
||||||
const blockTransactions = transactions.map(t => mempool[t.txid]);
|
// add the final unbounded block if it contains any transactions
|
||||||
restOfArray.forEach(tx => {
|
if (transactions.length > 0) {
|
||||||
blockWeight += tx.weight;
|
blocks.push(transactions.map(t => mempool[t.txid]));
|
||||||
tx.effectiveFeePerVsize = tx.feePerVsize;
|
|
||||||
tx.cpfpChecked = false;
|
|
||||||
blockTransactions.push(tx);
|
|
||||||
});
|
|
||||||
if (blockTransactions.length) {
|
|
||||||
blocks.push(blockTransactions);
|
|
||||||
}
|
}
|
||||||
transactions = [];
|
|
||||||
|
|
||||||
const end = Date.now();
|
const end = Date.now();
|
||||||
const time = end - start;
|
const time = end - start;
|
||||||
|
|||||||
@@ -45,8 +45,7 @@ class Server {
|
|||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
private server: http.Server | undefined;
|
private server: http.Server | undefined;
|
||||||
private app: Application;
|
private app: Application;
|
||||||
private currentBackendRetryInterval = 1;
|
private currentBackendRetryInterval = 5;
|
||||||
private backendRetryCount = 0;
|
|
||||||
|
|
||||||
private maxHeapSize: number = 0;
|
private maxHeapSize: number = 0;
|
||||||
private heapLogInterval: number = 60;
|
private heapLogInterval: number = 60;
|
||||||
@@ -180,22 +179,28 @@ class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
memPool.deleteExpiredTransactions();
|
memPool.deleteExpiredTransactions();
|
||||||
await blocks.$updateBlocks();
|
await Promise.race([
|
||||||
await memPool.$updateMempool();
|
blocks.$updateBlocks(),
|
||||||
|
new Promise((_, reject) => setTimeout(() => reject(new Error('$updateBlocks timed out')), 180000))
|
||||||
|
]);
|
||||||
|
await Promise.race([
|
||||||
|
memPool.$updateMempool(),
|
||||||
|
new Promise((_, reject) => setTimeout(() => reject(new Error('$updateMempool timed out')), 180000))
|
||||||
|
]);
|
||||||
indexer.$run();
|
indexer.$run();
|
||||||
|
|
||||||
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
|
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);
|
||||||
this.backendRetryCount = 0;
|
this.currentBackendRetryInterval = 5;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.backendRetryCount++;
|
let loggerMsg = `Exception in runMainUpdateLoop(). Retrying in ${this.currentBackendRetryInterval} sec.`;
|
||||||
let loggerMsg = `Exception in runMainUpdateLoop() (count: ${this.backendRetryCount}). Retrying in ${this.currentBackendRetryInterval} sec.`;
|
|
||||||
loggerMsg += ` Reason: ${(e instanceof Error ? e.message : e)}.`;
|
loggerMsg += ` Reason: ${(e instanceof Error ? e.message : e)}.`;
|
||||||
if (e?.stack) {
|
if (e?.stack) {
|
||||||
loggerMsg += ` Stack trace: ${e.stack}`;
|
loggerMsg += ` Stack trace: ${e.stack}`;
|
||||||
}
|
}
|
||||||
// When we get a first Exception, only `logger.debug` it and retry after 5 seconds
|
// When we get a first Exception, only `logger.debug` it and retry after 5 seconds
|
||||||
// From the second Exception, `logger.warn` the Exception and increase the retry delay
|
// From the second Exception, `logger.warn` the Exception and increase the retry delay
|
||||||
if (this.backendRetryCount >= 5) {
|
// Maximum retry delay is 60 seconds
|
||||||
|
if (this.currentBackendRetryInterval > 5) {
|
||||||
logger.warn(loggerMsg);
|
logger.warn(loggerMsg);
|
||||||
mempool.setOutOfSync();
|
mempool.setOutOfSync();
|
||||||
} else {
|
} else {
|
||||||
@@ -205,6 +210,8 @@ class Server {
|
|||||||
logger.debug(`AxiosError: ${e?.message}`);
|
logger.debug(`AxiosError: ${e?.message}`);
|
||||||
}
|
}
|
||||||
setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval);
|
setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval);
|
||||||
|
this.currentBackendRetryInterval *= 2;
|
||||||
|
this.currentBackendRetryInterval = Math.min(this.currentBackendRetryInterval, 60);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -205,7 +205,8 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
```json
|
```json
|
||||||
"ESPLORA": {
|
"ESPLORA": {
|
||||||
"REST_API_URL": "http://127.0.0.1:3000",
|
"REST_API_URL": "http://127.0.0.1:3000",
|
||||||
"UNIX_SOCKET_PATH": "/tmp/esplora-socket"
|
"UNIX_SOCKET_PATH": "/tmp/esplora-socket",
|
||||||
|
"RETRY_UNIX_SOCKET_AFTER": 30000
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -215,6 +216,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
environment:
|
environment:
|
||||||
ESPLORA_REST_API_URL: ""
|
ESPLORA_REST_API_URL: ""
|
||||||
ESPLORA_UNIX_SOCKET_PATH: ""
|
ESPLORA_UNIX_SOCKET_PATH: ""
|
||||||
|
ESPLORA_RETRY_UNIX_SOCKET_AFTER: ""
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,8 @@
|
|||||||
},
|
},
|
||||||
"ESPLORA": {
|
"ESPLORA": {
|
||||||
"REST_API_URL": "__ESPLORA_REST_API_URL__",
|
"REST_API_URL": "__ESPLORA_REST_API_URL__",
|
||||||
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__"
|
"UNIX_SOCKET_PATH": "__ESPLORA_UNIX_SOCKET_PATH__",
|
||||||
|
"RETRY_UNIX_SOCKET_AFTER": __ESPLORA_RETRY_UNIX_SOCKET_AFTER__
|
||||||
},
|
},
|
||||||
"SECOND_CORE_RPC": {
|
"SECOND_CORE_RPC": {
|
||||||
"HOST": "__SECOND_CORE_RPC_HOST__",
|
"HOST": "__SECOND_CORE_RPC_HOST__",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ __ELECTRUM_TLS_ENABLED__=${ELECTRUM_TLS_ENABLED:=false}
|
|||||||
# ESPLORA
|
# ESPLORA
|
||||||
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
|
__ESPLORA_REST_API_URL__=${ESPLORA_REST_API_URL:=http://127.0.0.1:3000}
|
||||||
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:=null}
|
__ESPLORA_UNIX_SOCKET_PATH__=${ESPLORA_UNIX_SOCKET_PATH:=null}
|
||||||
|
__ESPLORA_RETRY_UNIX_SOCKET_AFTER__=${ESPLORA_RETRY_UNIX_SOCKET_AFTER:=30000}
|
||||||
|
|
||||||
# SECOND_CORE_RPC
|
# SECOND_CORE_RPC
|
||||||
__SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1}
|
__SECOND_CORE_RPC_HOST__=${SECOND_CORE_RPC_HOST:=127.0.0.1}
|
||||||
@@ -168,6 +169,7 @@ sed -i "s/__ELECTRUM_TLS_ENABLED__/${__ELECTRUM_TLS_ENABLED__}/g" mempool-config
|
|||||||
|
|
||||||
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
|
sed -i "s!__ESPLORA_REST_API_URL__!${__ESPLORA_REST_API_URL__}!g" mempool-config.json
|
||||||
sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
sed -i "s!__ESPLORA_UNIX_SOCKET_PATH__!${__ESPLORA_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||||
|
sed -i "s!__ESPLORA_RETRY_UNIX_SOCKET_AFTER__!${__ESPLORA_RETRY_UNIX_SOCKET_AFTER__}!g" mempool-config.json
|
||||||
|
|
||||||
sed -i "s/__SECOND_CORE_RPC_HOST__/${__SECOND_CORE_RPC_HOST__}/g" mempool-config.json
|
sed -i "s/__SECOND_CORE_RPC_HOST__/${__SECOND_CORE_RPC_HOST__}/g" mempool-config.json
|
||||||
sed -i "s/__SECOND_CORE_RPC_PORT__/${__SECOND_CORE_RPC_PORT__}/g" mempool-config.json
|
sed -i "s/__SECOND_CORE_RPC_PORT__/${__SECOND_CORE_RPC_PORT__}/g" mempool-config.json
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(difficultyAdjustments$ | async) as da;">
|
<div class="mempool-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(difficultyAdjustments$ | async) as da;">
|
||||||
<div class="flashing">
|
<div class="flashing">
|
||||||
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
|
<ng-template ngFor let-projectedBlock [ngForOf]="mempoolBlocks$ | async" let-i="index" [ngForTrackBy]="trackByFn">
|
||||||
<div @blockEntryTrigger [@.disabled]="!animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
|
<div @blockEntryTrigger [@.disabled]="i > 0 || !animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
|
||||||
<a draggable="false" [routerLink]="['/mempool-block/' | relativeUrl, i]"
|
<a draggable="false" [routerLink]="['/mempool-block/' | relativeUrl, i]"
|
||||||
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
||||||
<div class="block-body">
|
<div class="block-body">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, HostListener } from '@angular/core';
|
||||||
import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
|
import { Subscription, Observable, fromEvent, merge, of, combineLatest } from 'rxjs';
|
||||||
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
import { MempoolBlock } from '../../interfaces/websocket.interface';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
@@ -222,8 +222,13 @@ export class MempoolBlocksComponent implements OnInit, OnDestroy {
|
|||||||
clearTimeout(this.resetTransitionTimeout);
|
clearTimeout(this.resetTransitionTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HostListener('window:resize', ['$event'])
|
||||||
|
onResize(): void {
|
||||||
|
this.animateEntry = false;
|
||||||
|
}
|
||||||
|
|
||||||
trackByFn(index: number, block: MempoolBlock) {
|
trackByFn(index: number, block: MempoolBlock) {
|
||||||
return (block.isStack) ? 'stack' : block.index;
|
return (block.isStack) ? `stack-${block.index}` : block.index;
|
||||||
}
|
}
|
||||||
|
|
||||||
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
reduceMempoolBlocksToFitScreen(blocks: MempoolBlock[]): MempoolBlock[] {
|
||||||
|
|||||||
@@ -137,9 +137,11 @@ export class StartComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMouseDown(event: MouseEvent) {
|
onMouseDown(event: MouseEvent) {
|
||||||
this.mouseDragStartX = event.clientX;
|
if (!(event.which > 1 || event.button > 0)) {
|
||||||
this.resetMomentum(event.clientX);
|
this.mouseDragStartX = event.clientX;
|
||||||
this.blockchainScrollLeftInit = this.blockchainContainer.nativeElement.scrollLeft;
|
this.resetMomentum(event.clientX);
|
||||||
|
this.blockchainScrollLeftInit = this.blockchainContainer.nativeElement.scrollLeft;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onPointerDown(event: PointerEvent) {
|
onPointerDown(event: PointerEvent) {
|
||||||
if (this.isiOS) {
|
if (this.isiOS) {
|
||||||
|
|||||||
Reference in New Issue
Block a user