Merge branch 'master' into nymkappa/bugfix/price-update-invalid-response
This commit is contained in:
commit
71b373463b
19
.github/dependabot.yml
vendored
19
.github/dependabot.yml
vendored
@ -8,6 +8,9 @@ updates:
|
|||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "*"
|
- dependency-name: "*"
|
||||||
update-types: ["version-update:semver-major"]
|
update-types: ["version-update:semver-major"]
|
||||||
|
allow:
|
||||||
|
- dependency-type: "production"
|
||||||
|
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/frontend"
|
directory: "/frontend"
|
||||||
schedule:
|
schedule:
|
||||||
@ -16,17 +19,29 @@ updates:
|
|||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "*"
|
- dependency-name: "*"
|
||||||
update-types: ["version-update:semver-major"]
|
update-types: ["version-update:semver-major"]
|
||||||
|
allow:
|
||||||
|
- dependency-type: "production"
|
||||||
|
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: "/docker/backend"
|
directory: "/docker/backend"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: weekly
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "*"
|
- dependency-name: "*"
|
||||||
update-types: ["version-update:semver-major"]
|
update-types: ["version-update:semver-major"]
|
||||||
|
|
||||||
|
- package-ecosystem: docker
|
||||||
|
directory: "/docker/frontend"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
ignore:
|
||||||
|
- dependency-name: "*"
|
||||||
|
update-types: ["version-update:semver-major"]
|
||||||
|
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: weekly
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: "*"
|
- dependency-name: "*"
|
||||||
update-types: ["version-update:semver-major"]
|
update-types: ["version-update:semver-major"]
|
||||||
|
10
.github/workflows/on-tag.yml
vendored
10
.github/workflows/on-tag.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo swapoff /mnt/swapfile
|
sudo swapoff /mnt/swapfile
|
||||||
sudo rm -v /mnt/swapfile
|
sudo rm -v /mnt/swapfile
|
||||||
sudo fallocate -l 10G /mnt/swapfile
|
sudo fallocate -l 13G /mnt/swapfile
|
||||||
sudo chmod 600 /mnt/swapfile
|
sudo chmod 600 /mnt/swapfile
|
||||||
sudo mkswap /mnt/swapfile
|
sudo mkswap /mnt/swapfile
|
||||||
sudo swapon /mnt/swapfile
|
sudo swapon /mnt/swapfile
|
||||||
@ -68,24 +68,24 @@ jobs:
|
|||||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||||
|
|
||||||
- name: Checkout project
|
- name: Checkout project
|
||||||
uses: actions/checkout@e2f20e631ae6d7dd3b768f56a5d2af784dd54791 # v2.5.0
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Init repo for Dockerization
|
- name: Init repo for Dockerization
|
||||||
run: docker/init.sh "$TAG"
|
run: docker/init.sh "$TAG"
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@e81a89b1732b9c48d79cd809d8d81d79c4647a18 # v2.1.0
|
uses: docker/setup-qemu-action@v2
|
||||||
id: qemu
|
id: qemu
|
||||||
|
|
||||||
- name: Setup Docker buildx action
|
- name: Setup Docker buildx action
|
||||||
uses: docker/setup-buildx-action@8c0edbc76e98fa90f69d9a2c020dcb50019dc325 # v2.2.1
|
uses: docker/setup-buildx-action@v2
|
||||||
id: buildx
|
id: buildx
|
||||||
|
|
||||||
- name: Available platforms
|
- name: Available platforms
|
||||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||||
|
|
||||||
- name: Cache Docker layers
|
- name: Cache Docker layers
|
||||||
uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 # v3.0.11
|
uses: actions/cache@v3
|
||||||
id: cache
|
id: cache
|
||||||
with:
|
with:
|
||||||
path: /tmp/.buildx-cache
|
path: /tmp/.buildx-cache
|
||||||
|
@ -25,9 +25,10 @@
|
|||||||
"AUTOMATIC_BLOCK_REINDEXING": false,
|
"AUTOMATIC_BLOCK_REINDEXING": false,
|
||||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
|
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
|
||||||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||||
|
"AUDIT": false,
|
||||||
"ADVANCED_GBT_AUDIT": false,
|
"ADVANCED_GBT_AUDIT": false,
|
||||||
"ADVANCED_GBT_MEMPOOL": false,
|
"ADVANCED_GBT_MEMPOOL": false,
|
||||||
"TRANSACTION_INDEXING": false
|
"CPFP_INDEXING": false
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
|
648
backend/package-lock.json
generated
648
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -34,11 +34,11 @@
|
|||||||
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
|
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.20.5",
|
"@babel/core": "^7.20.12",
|
||||||
"@mempool/electrum-client": "^1.1.7",
|
"@mempool/electrum-client": "^1.1.7",
|
||||||
"@types/node": "^16.11.41",
|
"@types/node": "^16.18.11",
|
||||||
"axios": "~0.27.2",
|
"axios": "~0.27.2",
|
||||||
"bitcoinjs-lib": "~6.0.2",
|
"bitcoinjs-lib": "~6.1.0",
|
||||||
"crypto-js": "~4.1.1",
|
"crypto-js": "~4.1.1",
|
||||||
"express": "~4.18.2",
|
"express": "~4.18.2",
|
||||||
"maxmind": "~4.3.8",
|
"maxmind": "~4.3.8",
|
||||||
@ -49,19 +49,19 @@
|
|||||||
"ws": "~8.11.0"
|
"ws": "~8.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.5",
|
"@babel/core": "^7.20.7",
|
||||||
"@babel/code-frame": "^7.18.6",
|
"@babel/code-frame": "^7.18.6",
|
||||||
"@types/compression": "^1.7.2",
|
"@types/compression": "^1.7.2",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/express": "^4.17.14",
|
"@types/express": "^4.17.15",
|
||||||
"@types/jest": "^29.2.3",
|
"@types/jest": "^29.2.5",
|
||||||
"@types/ws": "~8.5.3",
|
"@types/ws": "~8.5.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.48.1",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.31.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.3.1",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.2",
|
||||||
"ts-jest": "^29.0.3",
|
"ts-jest": "^29.0.3",
|
||||||
"ts-node": "^10.9.1"
|
"ts-node": "^10.9.1"
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,10 @@
|
|||||||
"INDEXING_BLOCKS_AMOUNT": 14,
|
"INDEXING_BLOCKS_AMOUNT": 14,
|
||||||
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
|
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
|
||||||
"POOLS_JSON_URL": "__POOLS_JSON_URL__",
|
"POOLS_JSON_URL": "__POOLS_JSON_URL__",
|
||||||
"ADVANCED_GBT_AUDIT": "__ADVANCED_GBT_AUDIT__",
|
"AUDIT": "__MEMPOOL_AUDIT__",
|
||||||
"ADVANCED_GBT_MEMPOOL": "__ADVANCED_GBT_MEMPOOL__",
|
"ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__",
|
||||||
"TRANSACTION_INDEXING": "__TRANSACTION_INDEXING__"
|
"ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__",
|
||||||
|
"CPFP_INDEXING": "__MEMPOOL_CPFP_INDEXING__"
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "__CORE_RPC_HOST__",
|
"HOST": "__CORE_RPC_HOST__",
|
||||||
|
@ -38,9 +38,10 @@ describe('Mempool Backend Config', () => {
|
|||||||
STDOUT_LOG_MIN_PRIORITY: 'debug',
|
STDOUT_LOG_MIN_PRIORITY: 'debug',
|
||||||
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||||
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
|
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
|
||||||
|
AUDIT: false,
|
||||||
ADVANCED_GBT_AUDIT: false,
|
ADVANCED_GBT_AUDIT: false,
|
||||||
ADVANCED_GBT_MEMPOOL: false,
|
ADVANCED_GBT_MEMPOOL: false,
|
||||||
TRANSACTION_INDEXING: false,
|
CPFP_INDEXING: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
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 });
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
import { TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||||
import { Common } from './common';
|
|
||||||
import { TransactionExtended, MempoolBlockWithTransactions, AuditScore } from '../mempool.interfaces';
|
|
||||||
import blocksRepository from '../repositories/BlocksRepository';
|
|
||||||
import blocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
|
||||||
import blocks from '../api/blocks';
|
|
||||||
|
|
||||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { IBackendInfo } from '../mempool.interfaces';
|
import { IBackendInfo } from '../mempool.interfaces';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
class BackendInfo {
|
class BackendInfo {
|
||||||
private backendInfo: IBackendInfo;
|
private backendInfo: IBackendInfo;
|
||||||
@ -22,7 +23,8 @@ class BackendInfo {
|
|||||||
this.backendInfo = {
|
this.backendInfo = {
|
||||||
hostname: os.hostname(),
|
hostname: os.hostname(),
|
||||||
version: versionInfo.version,
|
version: versionInfo.version,
|
||||||
gitCommit: versionInfo.gitCommit
|
gitCommit: versionInfo.gitCommit,
|
||||||
|
lightning: config.LIGHTNING.ENABLED
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,4 +17,6 @@ function bitcoinApiFactory(): AbstractBitcoinApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const bitcoinCoreApi = new BitcoinApi(bitcoinClient);
|
||||||
|
|
||||||
export default bitcoinApiFactory();
|
export default bitcoinApiFactory();
|
||||||
|
@ -18,6 +18,7 @@ import blocks from '../blocks';
|
|||||||
import bitcoinClient from './bitcoin-client';
|
import bitcoinClient from './bitcoin-client';
|
||||||
import difficultyAdjustment from '../difficulty-adjustment';
|
import difficultyAdjustment from '../difficulty-adjustment';
|
||||||
import transactionRepository from '../../repositories/TransactionRepository';
|
import transactionRepository from '../../repositories/TransactionRepository';
|
||||||
|
import rbfCache from '../rbf-cache';
|
||||||
|
|
||||||
class BitcoinRoutes {
|
class BitcoinRoutes {
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application) {
|
||||||
@ -31,6 +32,8 @@ class BitcoinRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/replaces', this.getRbfHistory)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/cached', this.getCachedTx)
|
||||||
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
|
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@ -402,7 +405,8 @@ class BitcoinRoutes {
|
|||||||
private async getLegacyBlocks(req: Request, res: Response) {
|
private async getLegacyBlocks(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const returnBlocks: IEsploraApi.Block[] = [];
|
const returnBlocks: IEsploraApi.Block[] = [];
|
||||||
const fromHeight = parseInt(req.params.height, 10) || blocks.getCurrentBlockHeight();
|
const tip = blocks.getCurrentBlockHeight();
|
||||||
|
const fromHeight = Math.min(parseInt(req.params.height, 10) || tip, tip);
|
||||||
|
|
||||||
// Check if block height exist in local cache to skip the hash lookup
|
// Check if block height exist in local cache to skip the hash lookup
|
||||||
const blockByHeight = blocks.getBlocks().find((b) => b.height === fromHeight);
|
const blockByHeight = blocks.getBlocks().find((b) => b.height === fromHeight);
|
||||||
@ -588,6 +592,28 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getRbfHistory(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const result = rbfCache.getReplaces(req.params.txId);
|
||||||
|
res.json(result || []);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getCachedTx(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const result = rbfCache.getTx(req.params.txId);
|
||||||
|
if (result) {
|
||||||
|
res.json(result);
|
||||||
|
} else {
|
||||||
|
res.status(404).send('not found');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async getTransactionOutspends(req: Request, res: Response) {
|
private async getTransactionOutspends(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const result = await bitcoinApi.$getOutspends(req.params.txId);
|
const result = await bitcoinApi.$getOutspends(req.params.txId);
|
||||||
|
@ -17,17 +17,14 @@ import { prepareBlock } from '../utils/blocks-utils';
|
|||||||
import BlocksRepository from '../repositories/BlocksRepository';
|
import BlocksRepository from '../repositories/BlocksRepository';
|
||||||
import HashratesRepository from '../repositories/HashratesRepository';
|
import HashratesRepository from '../repositories/HashratesRepository';
|
||||||
import indexer from '../indexer';
|
import indexer from '../indexer';
|
||||||
import fiatConversion from './fiat-conversion';
|
|
||||||
import poolsParser from './pools-parser';
|
import poolsParser from './pools-parser';
|
||||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||||
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
import cpfpRepository from '../repositories/CpfpRepository';
|
import cpfpRepository from '../repositories/CpfpRepository';
|
||||||
import transactionRepository from '../repositories/TransactionRepository';
|
|
||||||
import mining from './mining/mining';
|
import mining from './mining/mining';
|
||||||
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
import DifficultyAdjustmentsRepository from '../repositories/DifficultyAdjustmentsRepository';
|
||||||
import PricesRepository from '../repositories/PricesRepository';
|
import PricesRepository from '../repositories/PricesRepository';
|
||||||
import priceUpdater from '../tasks/price-updater';
|
import priceUpdater from '../tasks/price-updater';
|
||||||
import { Block } from 'bitcoinjs-lib';
|
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private blocks: BlockExtended[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
@ -101,12 +98,23 @@ class Blocks {
|
|||||||
transactions.push(tx);
|
transactions.push(tx);
|
||||||
transactionsFetched++;
|
transactionsFetched++;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (i === 0) {
|
try {
|
||||||
const msg = `Cannot fetch coinbase tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e);
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
logger.err(msg);
|
// Try again with core
|
||||||
throw new Error(msg);
|
const tx = await transactionUtils.$getTransactionExtended(txIds[i], false, false, true);
|
||||||
} else {
|
transactions.push(tx);
|
||||||
logger.err(`Cannot fetch tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e));
|
transactionsFetched++;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (i === 0) {
|
||||||
|
const msg = `Cannot fetch coinbase tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e);
|
||||||
|
logger.err(msg);
|
||||||
|
throw new Error(msg);
|
||||||
|
} else {
|
||||||
|
logger.err(`Cannot fetch tx ${txIds[i]}. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,7 +169,7 @@ class Blocks {
|
|||||||
blockExtended.extras.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
|
blockExtended.extras.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
|
||||||
blockExtended.extras.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
|
blockExtended.extras.coinbaseTx = transactionUtils.stripCoinbaseTransaction(transactions[0]);
|
||||||
blockExtended.extras.coinbaseRaw = blockExtended.extras.coinbaseTx.vin[0].scriptsig;
|
blockExtended.extras.coinbaseRaw = blockExtended.extras.coinbaseTx.vin[0].scriptsig;
|
||||||
blockExtended.extras.usd = fiatConversion.getConversionRates().USD;
|
blockExtended.extras.usd = priceUpdater.latestPrices.USD;
|
||||||
|
|
||||||
if (block.height === 0) {
|
if (block.height === 0) {
|
||||||
blockExtended.extras.medianFee = 0; // 50th percentiles
|
blockExtended.extras.medianFee = 0; // 50th percentiles
|
||||||
@ -203,9 +211,11 @@ class Blocks {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(block.id);
|
if (config.MEMPOOL.AUDIT) {
|
||||||
if (auditScore != null) {
|
const auditScore = await BlocksAuditsRepository.$getBlockAuditScore(block.id);
|
||||||
blockExtended.extras.matchRate = auditScore.matchRate;
|
if (auditScore != null) {
|
||||||
|
blockExtended.extras.matchRate = auditScore.matchRate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,7 +306,7 @@ class Blocks {
|
|||||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||||
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
||||||
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
|
const progress = Math.round(totalIndexed / indexedBlocks.length * 10000) / 100;
|
||||||
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`);
|
logger.debug(`Indexing block summary for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
|
||||||
timer = new Date().getTime() / 1000;
|
timer = new Date().getTime() / 1000;
|
||||||
indexedThisRun = 0;
|
indexedThisRun = 0;
|
||||||
}
|
}
|
||||||
@ -309,12 +319,12 @@ class Blocks {
|
|||||||
newlyIndexed++;
|
newlyIndexed++;
|
||||||
}
|
}
|
||||||
if (newlyIndexed > 0) {
|
if (newlyIndexed > 0) {
|
||||||
logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`);
|
logger.notice(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`);
|
logger.debug(`Blocks summaries indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Blocks summaries indexing failed. Trying again in 10 seconds. 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)}`, logger.tags.mining);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -329,9 +339,10 @@ class Blocks {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all indexed block hash
|
// Get all indexed block hash
|
||||||
const unindexedBlocks = await blocksRepository.$getCPFPUnindexedBlocks();
|
const unindexedBlockHeights = await blocksRepository.$getCPFPUnindexedBlocks();
|
||||||
|
logger.info(`Indexing cpfp data for ${unindexedBlockHeights.length} blocks`);
|
||||||
|
|
||||||
if (!unindexedBlocks?.length) {
|
if (!unindexedBlockHeights?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,30 +351,26 @@ class Blocks {
|
|||||||
let countThisRun = 0;
|
let countThisRun = 0;
|
||||||
let timer = new Date().getTime() / 1000;
|
let timer = new Date().getTime() / 1000;
|
||||||
const startedAt = new Date().getTime() / 1000;
|
const startedAt = new Date().getTime() / 1000;
|
||||||
|
for (const height of unindexedBlockHeights) {
|
||||||
for (const block of unindexedBlocks) {
|
|
||||||
// Logging
|
// Logging
|
||||||
|
const hash = await bitcoinApi.$getBlockHash(height);
|
||||||
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
const elapsedSeconds = Math.max(1, new Date().getTime() / 1000 - timer);
|
||||||
if (elapsedSeconds > 5) {
|
if (elapsedSeconds > 5) {
|
||||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||||
const blockPerSeconds = Math.max(1, countThisRun / elapsedSeconds);
|
const blockPerSeconds = (countThisRun / elapsedSeconds);
|
||||||
const progress = Math.round(count / unindexedBlocks.length * 10000) / 100;
|
const progress = Math.round(count / unindexedBlockHeights.length * 10000) / 100;
|
||||||
logger.debug(`Indexing cpfp clusters for #${block.height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlocks.length} (${progress}%) | elapsed: ${runningFor} seconds`);
|
logger.debug(`Indexing cpfp clusters for #${height} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${count}/${unindexedBlockHeights.length} (${progress}%) | elapsed: ${runningFor} seconds`);
|
||||||
timer = new Date().getTime() / 1000;
|
timer = new Date().getTime() / 1000;
|
||||||
countThisRun = 0;
|
countThisRun = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.$indexCPFP(block.hash, block.height); // Calculate and save CPFP data for transactions in this block
|
await this.$indexCPFP(hash, height); // Calculate and save CPFP data for transactions in this block
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
count++;
|
count++;
|
||||||
countThisRun++;
|
countThisRun++;
|
||||||
}
|
}
|
||||||
if (count > 0) {
|
logger.notice(`CPFP indexing completed: indexed ${count} blocks`);
|
||||||
logger.notice(`CPFP indexing completed: indexed ${count} blocks`);
|
|
||||||
} else {
|
|
||||||
logger.debug(`CPFP indexing completed: indexed ${count} blocks`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`CPFP indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
|
logger.err(`CPFP indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`);
|
||||||
throw e;
|
throw e;
|
||||||
@ -385,7 +392,7 @@ class Blocks {
|
|||||||
|
|
||||||
const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
|
const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
|
||||||
|
|
||||||
logger.debug(`Indexing blocks from #${currentBlockHeight} to #${lastBlockToIndex}`);
|
logger.debug(`Indexing blocks from #${currentBlockHeight} to #${lastBlockToIndex}`, logger.tags.mining);
|
||||||
loadingIndicators.setProgress('block-indexing', 0);
|
loadingIndicators.setProgress('block-indexing', 0);
|
||||||
|
|
||||||
const chunkSize = 10000;
|
const chunkSize = 10000;
|
||||||
@ -405,7 +412,7 @@ class Blocks {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`);
|
logger.info(`Indexing ${missingBlockHeights.length} blocks from #${currentBlockHeight} to #${endBlock}`, logger.tags.mining);
|
||||||
|
|
||||||
for (const blockHeight of missingBlockHeights) {
|
for (const blockHeight of missingBlockHeights) {
|
||||||
if (blockHeight < lastBlockToIndex) {
|
if (blockHeight < lastBlockToIndex) {
|
||||||
@ -418,7 +425,7 @@ class Blocks {
|
|||||||
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
const runningFor = Math.max(1, Math.round((new Date().getTime() / 1000) - startedAt));
|
||||||
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
const blockPerSeconds = Math.max(1, indexedThisRun / elapsedSeconds);
|
||||||
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
|
const progress = Math.round(totalIndexed / indexingBlockAmount * 10000) / 100;
|
||||||
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds`);
|
logger.debug(`Indexing block #${blockHeight} | ~${blockPerSeconds.toFixed(2)} blocks/sec | total: ${totalIndexed}/${indexingBlockAmount} (${progress}%) | elapsed: ${runningFor} seconds`, logger.tags.mining);
|
||||||
timer = new Date().getTime() / 1000;
|
timer = new Date().getTime() / 1000;
|
||||||
indexedThisRun = 0;
|
indexedThisRun = 0;
|
||||||
loadingIndicators.setProgress('block-indexing', progress, false);
|
loadingIndicators.setProgress('block-indexing', progress, false);
|
||||||
@ -435,13 +442,13 @@ class Blocks {
|
|||||||
currentBlockHeight -= chunkSize;
|
currentBlockHeight -= chunkSize;
|
||||||
}
|
}
|
||||||
if (newlyIndexed > 0) {
|
if (newlyIndexed > 0) {
|
||||||
logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`);
|
logger.notice(`Block indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Block indexing completed: indexed ${newlyIndexed} blocks`);
|
logger.debug(`Block indexing completed: indexed ${newlyIndexed} blocks`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
loadingIndicators.setProgress('block-indexing', 100);
|
loadingIndicators.setProgress('block-indexing', 100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e));
|
logger.err('Block indexing failed. Trying again in 10 seconds. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
||||||
loadingIndicators.setProgress('block-indexing', 100);
|
loadingIndicators.setProgress('block-indexing', 100);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -519,7 +526,7 @@ class Blocks {
|
|||||||
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);
|
||||||
await this.$getStrippedBlockTransactions(newBlock.id, true, true);
|
await this.$getStrippedBlockTransactions(newBlock.id, true, true);
|
||||||
if (config.MEMPOOL.TRANSACTION_INDEXING) {
|
if (config.MEMPOOL.CPFP_INDEXING) {
|
||||||
await this.$indexCPFP(newBlock.id, lastBlock['height'] - i);
|
await this.$indexCPFP(newBlock.id, lastBlock['height'] - i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -537,7 +544,7 @@ class Blocks {
|
|||||||
priceId: lastestPriceId,
|
priceId: lastestPriceId,
|
||||||
}]);
|
}]);
|
||||||
} else {
|
} else {
|
||||||
logger.info(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`)
|
logger.info(`Cannot save block price for ${blockExtended.height} because the price updater hasnt completed yet. Trying again in 10 seconds.`, logger.tags.mining);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
indexer.runSingleTask('blocksPrices');
|
indexer.runSingleTask('blocksPrices');
|
||||||
}, 10000);
|
}, 10000);
|
||||||
@ -547,7 +554,7 @@ class Blocks {
|
|||||||
if (Common.blocksSummariesIndexingEnabled() === true) {
|
if (Common.blocksSummariesIndexingEnabled() === true) {
|
||||||
await this.$getStrippedBlockTransactions(blockExtended.id, true);
|
await this.$getStrippedBlockTransactions(blockExtended.id, true);
|
||||||
}
|
}
|
||||||
if (config.MEMPOOL.TRANSACTION_INDEXING) {
|
if (config.MEMPOOL.CPFP_INDEXING) {
|
||||||
this.$indexCPFP(blockExtended.id, this.currentBlockHeight);
|
this.$indexCPFP(blockExtended.id, this.currentBlockHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -603,7 +610,9 @@ class Blocks {
|
|||||||
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
|
const transactions = await this.$getTransactionsExtended(blockHash, block.height, true);
|
||||||
const blockExtended = await this.$getBlockExtended(block, transactions);
|
const blockExtended = await this.$getBlockExtended(block, transactions);
|
||||||
|
|
||||||
await blocksRepository.$saveBlockInDatabase(blockExtended);
|
if (Common.indexingEnabled()) {
|
||||||
|
await blocksRepository.$saveBlockInDatabase(blockExtended);
|
||||||
|
}
|
||||||
|
|
||||||
return prepareBlock(blockExtended);
|
return prepareBlock(blockExtended);
|
||||||
}
|
}
|
||||||
@ -677,7 +686,12 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
|
public async $getBlocks(fromHeight?: number, limit: number = 15): Promise<BlockExtended[]> {
|
||||||
let currentHeight = fromHeight !== undefined ? fromHeight : await blocksRepository.$mostRecentBlockHeight();
|
|
||||||
|
let currentHeight = fromHeight !== undefined ? fromHeight : this.currentBlockHeight;
|
||||||
|
if (currentHeight > this.currentBlockHeight) {
|
||||||
|
limit -= currentHeight - this.currentBlockHeight;
|
||||||
|
currentHeight = this.currentBlockHeight;
|
||||||
|
}
|
||||||
const returnBlocks: BlockExtended[] = [];
|
const returnBlocks: BlockExtended[] = [];
|
||||||
|
|
||||||
if (currentHeight < 0) {
|
if (currentHeight < 0) {
|
||||||
@ -702,7 +716,7 @@ class Blocks {
|
|||||||
block = await this.$indexBlock(currentHeight);
|
block = await this.$indexBlock(currentHeight);
|
||||||
returnBlocks.push(block);
|
returnBlocks.push(block);
|
||||||
} else if (nextHash != null) {
|
} else if (nextHash != null) {
|
||||||
block = prepareBlock(await bitcoinClient.getBlock(nextHash));
|
block = await this.$indexBlock(currentHeight);
|
||||||
nextHash = block.previousblockhash;
|
nextHash = block.previousblockhash;
|
||||||
returnBlocks.push(block);
|
returnBlocks.push(block);
|
||||||
}
|
}
|
||||||
@ -741,34 +755,15 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $indexCPFP(hash: string, height: number): Promise<void> {
|
public async $indexCPFP(hash: string, height: number): Promise<void> {
|
||||||
let transactions;
|
const block = await bitcoinClient.getBlock(hash, 2);
|
||||||
if (Common.blocksSummariesIndexingEnabled()) {
|
const transactions = block.tx.map(tx => {
|
||||||
transactions = await this.$getStrippedBlockTransactions(hash);
|
tx.vsize = tx.weight / 4;
|
||||||
const rawBlock = await bitcoinApi.$getRawBlock(hash);
|
tx.fee *= 100_000_000;
|
||||||
const block = Block.fromBuffer(rawBlock);
|
return tx;
|
||||||
const txMap = {};
|
});
|
||||||
for (const tx of block.transactions || []) {
|
|
||||||
txMap[tx.getId()] = tx;
|
const clusters: any[] = [];
|
||||||
}
|
|
||||||
for (const tx of transactions) {
|
|
||||||
// convert from bitcoinjs to esplora vin format
|
|
||||||
if (txMap[tx.txid]?.ins) {
|
|
||||||
tx.vin = txMap[tx.txid].ins.map(vin => {
|
|
||||||
return {
|
|
||||||
txid: vin.hash.slice().reverse().toString('hex')
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const block = await bitcoinClient.getBlock(hash, 2);
|
|
||||||
transactions = block.tx.map(tx => {
|
|
||||||
tx.vsize = tx.weight / 4;
|
|
||||||
tx.fee *= 100_000_000;
|
|
||||||
return tx;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let cluster: TransactionStripped[] = [];
|
let cluster: TransactionStripped[] = [];
|
||||||
let ancestors: { [txid: string]: boolean } = {};
|
let ancestors: { [txid: string]: boolean } = {};
|
||||||
for (let i = transactions.length - 1; i >= 0; i--) {
|
for (let i = transactions.length - 1; i >= 0; i--) {
|
||||||
@ -782,10 +777,12 @@ class Blocks {
|
|||||||
});
|
});
|
||||||
const effectiveFeePerVsize = totalFee / totalVSize;
|
const effectiveFeePerVsize = totalFee / totalVSize;
|
||||||
if (cluster.length > 1) {
|
if (cluster.length > 1) {
|
||||||
await cpfpRepository.$saveCluster(height, cluster.map(tx => { return { txid: tx.txid, weight: tx.vsize * 4, fee: tx.fee || 0 }; }), effectiveFeePerVsize);
|
clusters.push({
|
||||||
for (const tx of cluster) {
|
root: cluster[0].txid,
|
||||||
await transactionRepository.$setCluster(tx.txid, cluster[0].txid);
|
height,
|
||||||
}
|
txs: cluster.map(tx => { return { txid: tx.txid, weight: tx.vsize * 4, fee: tx.fee || 0 }; }),
|
||||||
|
effectiveFeePerVsize,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
cluster = [];
|
cluster = [];
|
||||||
ancestors = {};
|
ancestors = {};
|
||||||
@ -795,7 +792,10 @@ class Blocks {
|
|||||||
ancestors[vin.txid] = true;
|
ancestors[vin.txid] = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await blocksRepository.$setCPFPIndexed(hash);
|
const result = await cpfpRepository.$batchSaveClusters(clusters);
|
||||||
|
if (!result) {
|
||||||
|
await cpfpRepository.$insertProgressMarker(height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,24 +35,31 @@ export class Common {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getFeesInRange(transactions: TransactionExtended[], rangeLength: number) {
|
static getFeesInRange(transactions: TransactionExtended[], rangeLength: number) {
|
||||||
const arr = [transactions[transactions.length - 1].effectiveFeePerVsize];
|
const filtered: TransactionExtended[] = [];
|
||||||
|
let lastValidRate = Infinity;
|
||||||
|
// filter out anomalous fee rates to ensure monotonic range
|
||||||
|
for (const tx of transactions) {
|
||||||
|
if (tx.effectiveFeePerVsize <= lastValidRate) {
|
||||||
|
filtered.push(tx);
|
||||||
|
lastValidRate = tx.effectiveFeePerVsize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const arr = [filtered[filtered.length - 1].effectiveFeePerVsize];
|
||||||
const chunk = 1 / (rangeLength - 1);
|
const chunk = 1 / (rangeLength - 1);
|
||||||
let itemsToAdd = rangeLength - 2;
|
let itemsToAdd = rangeLength - 2;
|
||||||
|
|
||||||
while (itemsToAdd > 0) {
|
while (itemsToAdd > 0) {
|
||||||
arr.push(transactions[Math.floor(transactions.length * chunk * itemsToAdd)].effectiveFeePerVsize);
|
arr.push(filtered[Math.floor(filtered.length * chunk * itemsToAdd)].effectiveFeePerVsize);
|
||||||
itemsToAdd--;
|
itemsToAdd--;
|
||||||
}
|
}
|
||||||
|
|
||||||
arr.push(transactions[0].effectiveFeePerVsize);
|
arr.push(filtered[0].effectiveFeePerVsize);
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended } {
|
static findRbfTransactions(added: TransactionExtended[], deleted: TransactionExtended[]): { [txid: string]: TransactionExtended } {
|
||||||
const matches: { [txid: string]: TransactionExtended } = {};
|
const matches: { [txid: string]: TransactionExtended } = {};
|
||||||
deleted
|
deleted
|
||||||
// The replaced tx must have at least one input with nSequence < maxint-1 (That’s the opt-in)
|
|
||||||
.filter((tx) => tx.vin.some((vin) => vin.sequence < 0xfffffffe))
|
|
||||||
.forEach((deletedTx) => {
|
.forEach((deletedTx) => {
|
||||||
const foundMatches = added.find((addedTx) => {
|
const foundMatches = added.find((addedTx) => {
|
||||||
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
|
// The new tx must, absolutely speaking, pay at least as much fee as the replaced tx.
|
||||||
@ -61,7 +68,7 @@ export class Common {
|
|||||||
&& addedTx.feePerVsize > deletedTx.feePerVsize
|
&& addedTx.feePerVsize > deletedTx.feePerVsize
|
||||||
// Spends one or more of the same inputs
|
// Spends one or more of the same inputs
|
||||||
&& deletedTx.vin.some((deletedVin) =>
|
&& deletedTx.vin.some((deletedVin) =>
|
||||||
addedTx.vin.some((vin) => vin.txid === deletedVin.txid));
|
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
||||||
});
|
});
|
||||||
if (foundMatches) {
|
if (foundMatches) {
|
||||||
matches[deletedTx.txid] = foundMatches;
|
matches[deletedTx.txid] = foundMatches;
|
||||||
@ -190,7 +197,7 @@ export class Common {
|
|||||||
static cpfpIndexingEnabled(): boolean {
|
static cpfpIndexingEnabled(): boolean {
|
||||||
return (
|
return (
|
||||||
Common.indexingEnabled() &&
|
Common.indexingEnabled() &&
|
||||||
config.MEMPOOL.TRANSACTION_INDEXING === true
|
config.MEMPOOL.CPFP_INDEXING === true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,9 +2,12 @@ import config from '../config';
|
|||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
|
import blocksRepository from '../repositories/BlocksRepository';
|
||||||
|
import cpfpRepository from '../repositories/CpfpRepository';
|
||||||
|
import { RowDataPacket } from 'mysql2';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 49;
|
private static currentVersion = 52;
|
||||||
private queryTimeout = 3600_000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -442,6 +445,29 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('TRUNCATE TABLE `blocks_audits`');
|
await this.$executeQuery('TRUNCATE TABLE `blocks_audits`');
|
||||||
await this.updateToSchemaVersion(49);
|
await this.updateToSchemaVersion(49);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 50) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `blocks` DROP COLUMN `cpfp_indexed`');
|
||||||
|
await this.updateToSchemaVersion(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 51) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `cpfp_clusters` ADD INDEX `height` (`height`)');
|
||||||
|
await this.updateToSchemaVersion(51);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 52) {
|
||||||
|
await this.$executeQuery(this.getCreateCompactCPFPTableQuery(), await this.$checkIfTableExists('compact_cpfp_clusters'));
|
||||||
|
await this.$executeQuery(this.getCreateCompactTransactionsTableQuery(), await this.$checkIfTableExists('compact_transactions'));
|
||||||
|
try {
|
||||||
|
await this.$convertCompactCpfpTables();
|
||||||
|
await this.$executeQuery('DROP TABLE IF EXISTS `transactions`');
|
||||||
|
await this.$executeQuery('DROP TABLE IF EXISTS `cpfp_clusters`');
|
||||||
|
await this.updateToSchemaVersion(52);
|
||||||
|
} catch(e) {
|
||||||
|
logger.warn('' + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -913,6 +939,25 @@ class DatabaseMigration {
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getCreateCompactCPFPTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS compact_cpfp_clusters (
|
||||||
|
root binary(32) NOT NULL,
|
||||||
|
height int(10) NOT NULL,
|
||||||
|
txs BLOB DEFAULT NULL,
|
||||||
|
fee_rate float unsigned,
|
||||||
|
PRIMARY KEY (root),
|
||||||
|
INDEX (height)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCreateCompactTransactionsTableQuery(): string {
|
||||||
|
return `CREATE TABLE IF NOT EXISTS compact_transactions (
|
||||||
|
txid binary(32) NOT NULL,
|
||||||
|
cluster binary(32) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (txid)
|
||||||
|
) 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'];
|
||||||
|
|
||||||
@ -933,6 +978,49 @@ class DatabaseMigration {
|
|||||||
logger.warn(`Unable to erase indexed data`);
|
logger.warn(`Unable to erase indexed data`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async $convertCompactCpfpTables(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const batchSize = 250;
|
||||||
|
const maxHeight = await blocksRepository.$mostRecentBlockHeight() || 0;
|
||||||
|
const [minHeightRows]: any = await DB.query(`SELECT MIN(height) AS minHeight from cpfp_clusters`);
|
||||||
|
const minHeight = (minHeightRows.length && minHeightRows[0].minHeight != null) ? minHeightRows[0].minHeight : maxHeight;
|
||||||
|
let height = maxHeight;
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
let timer = new Date().getTime() / 1000;
|
||||||
|
const startedAt = new Date().getTime() / 1000;
|
||||||
|
|
||||||
|
while (height > minHeight) {
|
||||||
|
const [rows] = await DB.query(
|
||||||
|
`
|
||||||
|
SELECT * from cpfp_clusters
|
||||||
|
WHERE height <= ? AND height > ?
|
||||||
|
ORDER BY height
|
||||||
|
`,
|
||||||
|
[height, height - batchSize]
|
||||||
|
) as RowDataPacket[][];
|
||||||
|
if (rows?.length) {
|
||||||
|
await cpfpRepository.$batchSaveClusters(rows.map(row => {
|
||||||
|
return {
|
||||||
|
root: row.root,
|
||||||
|
height: row.height,
|
||||||
|
txs: JSON.parse(row.txs),
|
||||||
|
effectiveFeePerVsize: row.fee_rate,
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = new Date().getTime() / 1000 - timer;
|
||||||
|
const runningFor = new Date().getTime() / 1000 - startedAt;
|
||||||
|
logger.debug(`Migrated cpfp data from block ${height} to ${height - batchSize} in ${elapsed.toFixed(2)} seconds | total elapsed: ${runningFor.toFixed(2)} seconds`);
|
||||||
|
timer = new Date().getTime() / 1000;
|
||||||
|
height -= batchSize;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Failed to migrate cpfp transaction data`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new DatabaseMigration();
|
export default new DatabaseMigration();
|
||||||
|
@ -670,9 +670,7 @@ class ChannelsApi {
|
|||||||
AND status != 2
|
AND status != 2
|
||||||
`);
|
`);
|
||||||
if (result[0].changedRows ?? 0 > 0) {
|
if (result[0].changedRows ?? 0 > 0) {
|
||||||
logger.info(`Marked ${result[0].changedRows} channels as inactive because they are not in the graph`);
|
logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not in the graph`, logger.tags.ln);
|
||||||
} else {
|
|
||||||
logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not in the graph`);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
@ -685,9 +685,7 @@ class NodesApi {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
if (result[0].changedRows ?? 0 > 0) {
|
if (result[0].changedRows ?? 0 > 0) {
|
||||||
logger.info(`Marked ${result[0].changedRows} nodes as inactive because they are not in the graph`);
|
logger.debug(`Marked ${result[0].changedRows} nodes as inactive because they are not in the graph`, logger.tags.ln);
|
||||||
} else {
|
|
||||||
logger.debug(`Marked ${result[0].changedRows} nodes as inactive because they are not in the graph`);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$setNodesInactive() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$setNodesInactive() error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
@ -41,13 +41,70 @@ class NodesRoutes {
|
|||||||
let nodes: any[] = [];
|
let nodes: any[] = [];
|
||||||
switch (config.MEMPOOL.NETWORK) {
|
switch (config.MEMPOOL.NETWORK) {
|
||||||
case 'testnet':
|
case 'testnet':
|
||||||
nodesList = ['032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b', '025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7', '0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55', '032ab2028c0b614c6d87824e2373529652fd7e4221b4c70cc4da7c7005c49afcf0', '029001b22fe70b48bee12d014df91982eb85ff1bd404ec772d5c83c4ee3e88d2c3', '0212e2848d79f928411da5f2ff0a8c95ec6ccb5a09d2031b6f71e91309dcde63af', '03e871a2229523d34f76e6311ff197cfe7f26c2fbec13554b93a46f4e710c47dab', '032202ec98d976b0e928bd1d91924e8bd3eab07231fc39feb3737b010071073df8', '02fa7c5a948d03d563a9f36940c2205a814e594d17c0042ced242c71a857d72605', '039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205', '033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18', '029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584'];
|
nodesList = [
|
||||||
|
'032c7c7819276c4f706a04df1a0f1e10a5495994a7be4c1d3d28ca766e5a2b957b',
|
||||||
|
'025a7e38c2834dd843591a4d23d5f09cdeb77ddca85f673c2d944a14220ff14cf7',
|
||||||
|
'0395e2731a1673ef21d7a16a727c4fc4d4c35a861c428ce2c819c53d2b81c8bd55',
|
||||||
|
'032ab2028c0b614c6d87824e2373529652fd7e4221b4c70cc4da7c7005c49afcf0',
|
||||||
|
'029001b22fe70b48bee12d014df91982eb85ff1bd404ec772d5c83c4ee3e88d2c3',
|
||||||
|
'0212e2848d79f928411da5f2ff0a8c95ec6ccb5a09d2031b6f71e91309dcde63af',
|
||||||
|
'03e871a2229523d34f76e6311ff197cfe7f26c2fbec13554b93a46f4e710c47dab',
|
||||||
|
'032202ec98d976b0e928bd1d91924e8bd3eab07231fc39feb3737b010071073df8',
|
||||||
|
'02fa7c5a948d03d563a9f36940c2205a814e594d17c0042ced242c71a857d72605',
|
||||||
|
'039c14fdec2d958e3d14cebf657451bbd9e039196615785e82c917f274e3fb2205',
|
||||||
|
'033589bbcb233ffc416cefd5437c7f37e9d7cb7942d405e39e72c4c846d9b37f18',
|
||||||
|
'029293110441c6e2eacb57e1255bf6ef05c41a6a676fe474922d33c19f98a7d584',
|
||||||
|
'0235ad0b56ed8c42c4354444c24e971c05e769ec0b5fb0ccea42880095dc02ea2c',
|
||||||
|
'029700819a37afea630f80e6cc461f3fd3c4ace2598a21cfbbe64d1c78d0ee69a5',
|
||||||
|
'02c2d8b2dbf87c7894af2f1d321290e2fe6db5446cd35323987cee98f06e2e0075',
|
||||||
|
'030b0ca1ea7b1075716d2a555630e6fd47ef11bc7391fe68963ec06cf370a5e382',
|
||||||
|
'031adb9eb2d66693f85fa31a4adca0319ba68219f3ad5f9a2ef9b34a6b40755fa1',
|
||||||
|
'02ccd07faa47eda810ecf5591ccf5ca50f6c1034d0d175052898d32a00b9bae24f',
|
||||||
|
];
|
||||||
break;
|
break;
|
||||||
case 'signet':
|
case 'signet':
|
||||||
nodesList = ['03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956', '033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de', '02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781', '025196512905b8a3f1597428b867bec63ec9a95e5089eb7dc7e63e2d2691669029', '027c625aa1fbe3768db68ebcb05b53b6dc0ce68b7b54b8900d326d167363e684fe', '03f1629af3101fcc56b7aac2667016be84e3defbf3d0c8719f836c9b41c9a57a43', '02dfb81e2f7a3c4c9e8a51b70ef82b4a24549cc2fab1f5b2fd636501774a918991', '02d01ccf832944c68f10d39006093769c5b8bda886d561b128534e313d729fdb34', '02499ed23027d4698a6904ff4ec1b6085a61f10b9a6937f90438f9947e38e8ea86', '038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7', '03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761', '028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7'];
|
nodesList = [
|
||||||
|
'03ddab321b760433cbf561b615ef62ac7d318630c5f51d523aaf5395b90b751956',
|
||||||
|
'033d92c7bfd213ef1b34c90e985fb5dc77f9ec2409d391492484e57a44c4aca1de',
|
||||||
|
'02ad010dda54253c1eb9efe38b0760657a3b43ecad62198c359c051c9d99d45781',
|
||||||
|
'025196512905b8a3f1597428b867bec63ec9a95e5089eb7dc7e63e2d2691669029',
|
||||||
|
'027c625aa1fbe3768db68ebcb05b53b6dc0ce68b7b54b8900d326d167363e684fe',
|
||||||
|
'03f1629af3101fcc56b7aac2667016be84e3defbf3d0c8719f836c9b41c9a57a43',
|
||||||
|
'02dfb81e2f7a3c4c9e8a51b70ef82b4a24549cc2fab1f5b2fd636501774a918991',
|
||||||
|
'02d01ccf832944c68f10d39006093769c5b8bda886d561b128534e313d729fdb34',
|
||||||
|
'02499ed23027d4698a6904ff4ec1b6085a61f10b9a6937f90438f9947e38e8ea86',
|
||||||
|
'038310e3a786340f2bd7770704c7ccfe560fd163d9a1c99d67894597419d12cbf7',
|
||||||
|
'03e5e9d879b72c7d67ecd483bae023bd33e695bb32b981a4021260f7b9d62bc761',
|
||||||
|
'028d16e1a0ace4c0c0a421536d8d32ce484dfe6e2f726b7b0e7c30f12a195f8cc7',
|
||||||
|
'02ff690d06c187ab994bf83c5a2114fe5bf50112c2c817af0f788f736be9fa2070',
|
||||||
|
'02a9f570c51a2526a5ee85802e88f9281bed771eb66a0c8a7d898430dd5d0eae45',
|
||||||
|
'038c3de773255d3bd7a50e31e58d423baac5c90826a74d75e64b74c95475de1097',
|
||||||
|
'0242c7f7d315095f37ad1421ae0a2fc967d4cbe65b61b079c5395a769436959853',
|
||||||
|
'02a909e70eb03742f12666ebb1f56ac42a5fbaab0c0e8b5b1df4aa9f10f8a09240',
|
||||||
|
'03a26efa12489803c07f3ac2f1dba63812e38f0f6e866ce3ebb34df7de1f458cd2',
|
||||||
|
];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
nodesList = ['03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61', '03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437', '03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144', '0238bd27f02d67d6c51e269692bc8c9a32357a00e7777cba7f4f1f18a2a700b108', '03f983dcabed6baa1eab5b56c8b2e8fdc846ab3fd931155377897335e85a9fa57c', '03e399589533581e48796e29a825839a010036a61b20744fda929d6709fcbffcc5', '021f5288b5f72c42cd0d8801086af7ce09a816d8ee9a4c47a4b436399b26cb601a', '032b01b7585f781420cd4148841a82831ba37fa952342052cec16750852d4f2dd9', '02848036488d4b8fb1f1c4064261ec36151f43b085f0b51bd239ade3ddfc940c34', '02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf', '03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c', '0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43'];
|
nodesList = [
|
||||||
|
'03fbc17549ec667bccf397ababbcb4cdc0e3394345e4773079ab2774612ec9be61',
|
||||||
|
'03da9a8623241ccf95f19cd645c6cecd4019ac91570e976eb0a128bebbc4d8a437',
|
||||||
|
'03ca5340cf85cb2e7cf076e489f785410838de174e40be62723e8a60972ad75144',
|
||||||
|
'0238bd27f02d67d6c51e269692bc8c9a32357a00e7777cba7f4f1f18a2a700b108',
|
||||||
|
'03f983dcabed6baa1eab5b56c8b2e8fdc846ab3fd931155377897335e85a9fa57c',
|
||||||
|
'03e399589533581e48796e29a825839a010036a61b20744fda929d6709fcbffcc5',
|
||||||
|
'021f5288b5f72c42cd0d8801086af7ce09a816d8ee9a4c47a4b436399b26cb601a',
|
||||||
|
'032b01b7585f781420cd4148841a82831ba37fa952342052cec16750852d4f2dd9',
|
||||||
|
'02848036488d4b8fb1f1c4064261ec36151f43b085f0b51bd239ade3ddfc940c34',
|
||||||
|
'02b6b1640fe029e304c216951af9fbefdb23b0bdc9baaf327540d31b6107841fdf',
|
||||||
|
'03694289827203a5b3156d753071ddd5bf92e371f5a462943f9555eef6d2d6606c',
|
||||||
|
'0283d850db7c3e8ea7cc9c4abc7afaab12bbdf72b677dcba1d608350d2537d7d43',
|
||||||
|
'02521287789f851268a39c9eccc9d6180d2c614315b583c9e6ae0addbd6d79df06',
|
||||||
|
'0258c2a7b7f8af2585b4411b1ec945f70988f30412bb1df179de941f14d0b1bc3e',
|
||||||
|
'03c3389ff1a896f84d921ed01a19fc99c6724ce8dc4b960cd3b7b2362b62cd60d7',
|
||||||
|
'038d118996b3eaa15dcd317b32a539c9ecfdd7698f204acf8a087336af655a9192',
|
||||||
|
'02a928903d93d78877dacc3642b696128a3636e9566dd42d2d132325b2c8891c09',
|
||||||
|
'0328cd17f3a9d3d90b532ade0d1a67e05eb8a51835b3dce0a2e38eac04b5a62a57',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let pubKey of nodesList) {
|
for (let pubKey of nodesList) {
|
||||||
|
@ -1,123 +0,0 @@
|
|||||||
import logger from '../logger';
|
|
||||||
import * as http from 'http';
|
|
||||||
import * as https from 'https';
|
|
||||||
import axios, { AxiosResponse } from 'axios';
|
|
||||||
import { IConversionRates } from '../mempool.interfaces';
|
|
||||||
import config from '../config';
|
|
||||||
import backendInfo from './backend-info';
|
|
||||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
|
||||||
|
|
||||||
class FiatConversion {
|
|
||||||
private debasingFiatCurrencies = ['AED', 'AUD', 'BDT', 'BHD', 'BMD', 'BRL', 'CAD', 'CHF', 'CLP',
|
|
||||||
'CNY', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', 'HUF', 'IDR', 'ILS', 'INR', 'JPY', 'KRW', 'KWD',
|
|
||||||
'LKR', 'MMK', 'MXN', 'MYR', 'NGN', 'NOK', 'NZD', 'PHP', 'PKR', 'PLN', 'RUB', 'SAR', 'SEK',
|
|
||||||
'SGD', 'THB', 'TRY', 'TWD', 'UAH', 'USD', 'VND', 'ZAR'];
|
|
||||||
private conversionRates: IConversionRates = {};
|
|
||||||
private ratesChangedCallback: ((rates: IConversionRates) => void) | undefined;
|
|
||||||
public ratesInitialized = false; // If true, it means rates are ready for use
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
for (const fiat of this.debasingFiatCurrencies) {
|
|
||||||
this.conversionRates[fiat] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public setProgressChangedCallback(fn: (rates: IConversionRates) => void) {
|
|
||||||
this.ratesChangedCallback = fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public startService() {
|
|
||||||
const fiatConversionUrl = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.PRICE_DATA_SERVER.TOR_URL : config.PRICE_DATA_SERVER.CLEARNET_URL;
|
|
||||||
logger.info('Starting currency rates service');
|
|
||||||
if (config.SOCKS5PROXY.ENABLED) {
|
|
||||||
logger.info(`Currency rates service will be queried over the Tor network using ${fiatConversionUrl}`);
|
|
||||||
} else {
|
|
||||||
logger.info(`Currency rates service will be queried over clearnet using ${config.PRICE_DATA_SERVER.CLEARNET_URL}`);
|
|
||||||
}
|
|
||||||
setInterval(this.updateCurrency.bind(this), 1000 * config.MEMPOOL.PRICE_FEED_UPDATE_INTERVAL);
|
|
||||||
this.updateCurrency();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getConversionRates() {
|
|
||||||
return this.conversionRates;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateCurrency(): Promise<void> {
|
|
||||||
type axiosOptions = {
|
|
||||||
headers: {
|
|
||||||
'User-Agent': string
|
|
||||||
};
|
|
||||||
timeout: number;
|
|
||||||
httpAgent?: http.Agent;
|
|
||||||
httpsAgent?: https.Agent;
|
|
||||||
}
|
|
||||||
const setDelay = (secs: number = 1): Promise<void> => new Promise(resolve => setTimeout(() => resolve(), secs * 1000));
|
|
||||||
const fiatConversionUrl = (config.SOCKS5PROXY.ENABLED === true) && (config.SOCKS5PROXY.USE_ONION === true) ? config.PRICE_DATA_SERVER.TOR_URL : config.PRICE_DATA_SERVER.CLEARNET_URL;
|
|
||||||
const isHTTP = (new URL(fiatConversionUrl).protocol.split(':')[0] === 'http') ? true : false;
|
|
||||||
const axiosOptions: axiosOptions = {
|
|
||||||
headers: {
|
|
||||||
'User-Agent': (config.MEMPOOL.USER_AGENT === 'mempool') ? `mempool/v${backendInfo.getBackendInfo().version}` : `${config.MEMPOOL.USER_AGENT}`
|
|
||||||
},
|
|
||||||
timeout: config.SOCKS5PROXY.ENABLED ? 30000 : 10000
|
|
||||||
};
|
|
||||||
|
|
||||||
let retry = 0;
|
|
||||||
|
|
||||||
while(retry < config.MEMPOOL.EXTERNAL_MAX_RETRY) {
|
|
||||||
try {
|
|
||||||
if (config.SOCKS5PROXY.ENABLED) {
|
|
||||||
let socksOptions: any = {
|
|
||||||
agentOptions: {
|
|
||||||
keepAlive: true,
|
|
||||||
},
|
|
||||||
hostname: config.SOCKS5PROXY.HOST,
|
|
||||||
port: config.SOCKS5PROXY.PORT
|
|
||||||
};
|
|
||||||
|
|
||||||
if (config.SOCKS5PROXY.USERNAME && config.SOCKS5PROXY.PASSWORD) {
|
|
||||||
socksOptions.username = config.SOCKS5PROXY.USERNAME;
|
|
||||||
socksOptions.password = config.SOCKS5PROXY.PASSWORD;
|
|
||||||
} else {
|
|
||||||
// Retry with different tor circuits https://stackoverflow.com/a/64960234
|
|
||||||
socksOptions.username = `circuit${retry}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle proxy agent for onion addresses
|
|
||||||
if (isHTTP) {
|
|
||||||
axiosOptions.httpAgent = new SocksProxyAgent(socksOptions);
|
|
||||||
} else {
|
|
||||||
axiosOptions.httpsAgent = new SocksProxyAgent(socksOptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug('Querying currency rates service...');
|
|
||||||
|
|
||||||
const response: AxiosResponse = await axios.get(`${fiatConversionUrl}`, axiosOptions);
|
|
||||||
|
|
||||||
if (response.statusText === 'error' || !response.data) {
|
|
||||||
throw new Error(`Could not fetch data from ${fiatConversionUrl}, Error: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const rate of response.data.data) {
|
|
||||||
if (this.debasingFiatCurrencies.includes(rate.currencyCode) && rate.provider === 'Bisq-Aggregate') {
|
|
||||||
this.conversionRates[rate.currencyCode] = Math.round(100 * rate.price) / 100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ratesInitialized = true;
|
|
||||||
logger.debug(`USD Conversion Rate: ${this.conversionRates.USD}`);
|
|
||||||
|
|
||||||
if (this.ratesChangedCallback) {
|
|
||||||
this.ratesChangedCallback(this.conversionRates);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
} catch (e) {
|
|
||||||
logger.err('Error updating fiat conversion rates: ' + (e instanceof Error ? e.message : e));
|
|
||||||
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
|
|
||||||
retry++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new FiatConversion();
|
|
@ -141,13 +141,13 @@ export default class CLightningClient extends EventEmitter implements AbstractLi
|
|||||||
// main data directory provided, default to using the bitcoin mainnet subdirectory
|
// main data directory provided, default to using the bitcoin mainnet subdirectory
|
||||||
// to be removed in v0.2.0
|
// to be removed in v0.2.0
|
||||||
else if (fExists(rpcPath, 'bitcoin', 'lightning-rpc')) {
|
else if (fExists(rpcPath, 'bitcoin', 'lightning-rpc')) {
|
||||||
logger.warn(`[CLightningClient] ${rpcPath}/lightning-rpc is missing, using the bitcoin mainnet subdirectory at ${rpcPath}/bitcoin instead.`)
|
logger.warn(`${rpcPath}/lightning-rpc is missing, using the bitcoin mainnet subdirectory at ${rpcPath}/bitcoin instead.`, logger.tags.ln)
|
||||||
logger.warn(`[CLightningClient] specifying the main lightning data directory is deprecated, please specify the network directory explicitly.\n`)
|
logger.warn(`specifying the main lightning data directory is deprecated, please specify the network directory explicitly.\n`, logger.tags.ln)
|
||||||
rpcPath = path.join(rpcPath, 'bitcoin', 'lightning-rpc')
|
rpcPath = path.join(rpcPath, 'bitcoin', 'lightning-rpc')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`[CLightningClient] Connecting to ${rpcPath}`);
|
logger.debug(`Connecting to ${rpcPath}`, logger.tags.ln);
|
||||||
|
|
||||||
super();
|
super();
|
||||||
this.rpcPath = rpcPath;
|
this.rpcPath = rpcPath;
|
||||||
@ -172,19 +172,19 @@ export default class CLightningClient extends EventEmitter implements AbstractLi
|
|||||||
|
|
||||||
this.clientConnectionPromise = new Promise<void>(resolve => {
|
this.clientConnectionPromise = new Promise<void>(resolve => {
|
||||||
_self.client.on('connect', () => {
|
_self.client.on('connect', () => {
|
||||||
logger.info(`[CLightningClient] Lightning client connected`);
|
logger.info(`CLightning client connected`, logger.tags.ln);
|
||||||
_self.reconnectWait = 1;
|
_self.reconnectWait = 1;
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
_self.client.on('end', () => {
|
_self.client.on('end', () => {
|
||||||
logger.err('[CLightningClient] Lightning client connection closed, reconnecting');
|
logger.err(`CLightning client connection closed, reconnecting`, logger.tags.ln);
|
||||||
_self.increaseWaitTime();
|
_self.increaseWaitTime();
|
||||||
_self.reconnect();
|
_self.reconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
_self.client.on('error', error => {
|
_self.client.on('error', error => {
|
||||||
logger.err(`[CLightningClient] Lightning client connection error: ${error}`);
|
logger.err(`CLightning client connection error: ${error}`, logger.tags.ln);
|
||||||
_self.increaseWaitTime();
|
_self.increaseWaitTime();
|
||||||
_self.reconnect();
|
_self.reconnect();
|
||||||
});
|
});
|
||||||
@ -196,7 +196,6 @@ export default class CLightningClient extends EventEmitter implements AbstractLi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = JSON.parse(line);
|
const data = JSON.parse(line);
|
||||||
// logger.debug(`[CLightningClient] #${data.id} <-- ${JSON.stringify(data.error || data.result)}`);
|
|
||||||
_self.emit('res:' + data.id, data);
|
_self.emit('res:' + data.id, data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -217,7 +216,7 @@ export default class CLightningClient extends EventEmitter implements AbstractLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.reconnectTimeout = setTimeout(() => {
|
this.reconnectTimeout = setTimeout(() => {
|
||||||
logger.debug('[CLightningClient] Trying to reconnect...');
|
logger.debug(`Trying to reconnect...`, logger.tags.ln);
|
||||||
|
|
||||||
_self.client.connect(_self.rpcPath);
|
_self.client.connect(_self.rpcPath);
|
||||||
_self.reconnectTimeout = null;
|
_self.reconnectTimeout = null;
|
||||||
@ -235,7 +234,6 @@ export default class CLightningClient extends EventEmitter implements AbstractLi
|
|||||||
id: '' + callInt
|
id: '' + callInt
|
||||||
};
|
};
|
||||||
|
|
||||||
// logger.debug(`[CLightningClient] #${callInt} --> ${method} ${args}`);
|
|
||||||
|
|
||||||
// Wait for the client to connect
|
// Wait for the client to connect
|
||||||
return this.clientConnectionPromise
|
return this.clientConnectionPromise
|
||||||
|
@ -2,6 +2,7 @@ import { ILightningApi } from '../lightning-api.interface';
|
|||||||
import FundingTxFetcher from '../../../tasks/lightning/sync-tasks/funding-tx-fetcher';
|
import FundingTxFetcher from '../../../tasks/lightning/sync-tasks/funding-tx-fetcher';
|
||||||
import logger from '../../../logger';
|
import logger from '../../../logger';
|
||||||
import { Common } from '../../common';
|
import { Common } from '../../common';
|
||||||
|
import config from '../../../config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a clightning "listnode" entry to a lnd node entry
|
* Convert a clightning "listnode" entry to a lnd node entry
|
||||||
@ -40,7 +41,7 @@ export function convertNode(clNode: any): ILightningApi.Node {
|
|||||||
* Convert clightning "listchannels" response to lnd "describegraph.edges" format
|
* Convert clightning "listchannels" response to lnd "describegraph.edges" format
|
||||||
*/
|
*/
|
||||||
export async function convertAndmergeBidirectionalChannels(clChannels: any[]): Promise<ILightningApi.Channel[]> {
|
export async function convertAndmergeBidirectionalChannels(clChannels: any[]): Promise<ILightningApi.Channel[]> {
|
||||||
logger.info('Converting clightning nodes and channels to lnd graph format');
|
logger.debug(`Converting clightning nodes and channels to lnd graph format`, logger.tags.ln);
|
||||||
|
|
||||||
let loggerTimer = new Date().getTime() / 1000;
|
let loggerTimer = new Date().getTime() / 1000;
|
||||||
let channelProcessed = 0;
|
let channelProcessed = 0;
|
||||||
@ -54,16 +55,17 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P
|
|||||||
clChannelsDict[clChannel.short_channel_id] = clChannel;
|
clChannelsDict[clChannel.short_channel_id] = clChannel;
|
||||||
clChannelsDictCount[clChannel.short_channel_id] = 1;
|
clChannelsDictCount[clChannel.short_channel_id] = 1;
|
||||||
} else {
|
} else {
|
||||||
consolidatedChannelList.push(
|
const fullChannel = await buildFullChannel(clChannel, clChannelsDict[clChannel.short_channel_id]);
|
||||||
await buildFullChannel(clChannel, clChannelsDict[clChannel.short_channel_id])
|
if (fullChannel !== null) {
|
||||||
);
|
consolidatedChannelList.push(fullChannel);
|
||||||
delete clChannelsDict[clChannel.short_channel_id];
|
delete clChannelsDict[clChannel.short_channel_id];
|
||||||
clChannelsDictCount[clChannel.short_channel_id]++;
|
clChannelsDictCount[clChannel.short_channel_id]++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
|
||||||
if (elapsedSeconds > 10) {
|
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
|
||||||
logger.info(`Building complete channels from clightning output. Channels processed: ${channelProcessed + 1} of ${clChannels.length}`);
|
logger.info(`Building complete channels from clightning output. Channels processed: ${channelProcessed + 1} of ${clChannels.length}`, logger.tags.ln);
|
||||||
loggerTimer = new Date().getTime() / 1000;
|
loggerTimer = new Date().getTime() / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,10 +75,13 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P
|
|||||||
channelProcessed = 0;
|
channelProcessed = 0;
|
||||||
const keys = Object.keys(clChannelsDict);
|
const keys = Object.keys(clChannelsDict);
|
||||||
for (const short_channel_id of keys) {
|
for (const short_channel_id of keys) {
|
||||||
consolidatedChannelList.push(await buildIncompleteChannel(clChannelsDict[short_channel_id]));
|
const incompleteChannel = await buildIncompleteChannel(clChannelsDict[short_channel_id]);
|
||||||
|
if (incompleteChannel !== null) {
|
||||||
|
consolidatedChannelList.push(incompleteChannel);
|
||||||
|
}
|
||||||
|
|
||||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
|
||||||
if (elapsedSeconds > 10) {
|
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
|
||||||
logger.info(`Building partial channels from clightning output. Channels processed: ${channelProcessed + 1} of ${keys.length}`);
|
logger.info(`Building partial channels from clightning output. Channels processed: ${channelProcessed + 1} of ${keys.length}`);
|
||||||
loggerTimer = new Date().getTime() / 1000;
|
loggerTimer = new Date().getTime() / 1000;
|
||||||
}
|
}
|
||||||
@ -91,10 +96,13 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P
|
|||||||
* Convert two clightning "getchannels" entries into a full a lnd "describegraph.edges" format
|
* Convert two clightning "getchannels" entries into a full a lnd "describegraph.edges" format
|
||||||
* In this case, clightning knows the channel policy for both nodes
|
* In this case, clightning knows the channel policy for both nodes
|
||||||
*/
|
*/
|
||||||
async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILightningApi.Channel> {
|
async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILightningApi.Channel | null> {
|
||||||
const lastUpdate = Math.max(clChannelA.last_update ?? 0, clChannelB.last_update ?? 0);
|
const lastUpdate = Math.max(clChannelA.last_update ?? 0, clChannelB.last_update ?? 0);
|
||||||
|
|
||||||
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannelA.short_channel_id);
|
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannelA.short_channel_id);
|
||||||
|
if (!tx) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const parts = clChannelA.short_channel_id.split('x');
|
const parts = clChannelA.short_channel_id.split('x');
|
||||||
const outputIdx = parts[2];
|
const outputIdx = parts[2];
|
||||||
|
|
||||||
@ -114,8 +122,11 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise<ILigh
|
|||||||
* Convert one clightning "getchannels" entry into a full a lnd "describegraph.edges" format
|
* Convert one clightning "getchannels" entry into a full a lnd "describegraph.edges" format
|
||||||
* In this case, clightning knows the channel policy of only one node
|
* In this case, clightning knows the channel policy of only one node
|
||||||
*/
|
*/
|
||||||
async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Channel> {
|
async function buildIncompleteChannel(clChannel: any): Promise<ILightningApi.Channel | null> {
|
||||||
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannel.short_channel_id);
|
const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannel.short_channel_id);
|
||||||
|
if (!tx) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const parts = clChannel.short_channel_id.split('x');
|
const parts = clChannel.short_channel_id.split('x');
|
||||||
const outputIdx = parts[2];
|
const outputIdx = parts[2];
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ class MempoolBlocks {
|
|||||||
return this.mempoolBlockDeltas;
|
return this.mempoolBlockDeltas;
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }): void {
|
public updateMempoolBlocks(memPool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): MempoolBlockWithTransactions[] {
|
||||||
const latestMempool = memPool;
|
const latestMempool = memPool;
|
||||||
const memPoolArray: TransactionExtended[] = [];
|
const memPoolArray: TransactionExtended[] = [];
|
||||||
for (const i in latestMempool) {
|
for (const i in latestMempool) {
|
||||||
@ -75,10 +75,14 @@ class MempoolBlocks {
|
|||||||
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
|
logger.debug('Mempool blocks calculated in ' + time / 1000 + ' seconds');
|
||||||
|
|
||||||
const blocks = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks);
|
const blocks = this.calculateMempoolBlocks(memPoolArray, this.mempoolBlocks);
|
||||||
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks);
|
|
||||||
|
|
||||||
this.mempoolBlocks = blocks;
|
if (saveResults) {
|
||||||
this.mempoolBlockDeltas = deltas;
|
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, blocks);
|
||||||
|
this.mempoolBlocks = blocks;
|
||||||
|
this.mempoolBlockDeltas = deltas;
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]): MempoolBlockWithTransactions[] {
|
private calculateMempoolBlocks(transactionsSorted: TransactionExtended[], prevBlocks: MempoolBlockWithTransactions[]): MempoolBlockWithTransactions[] {
|
||||||
@ -143,7 +147,7 @@ class MempoolBlocks {
|
|||||||
return mempoolBlockDeltas;
|
return mempoolBlockDeltas;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }): Promise<void> {
|
public async makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
|
||||||
// prepare a stripped down version of the mempool with only the minimum necessary data
|
// prepare a stripped down version of the mempool with only the minimum necessary data
|
||||||
// to reduce the overhead of passing this data to the worker thread
|
// to reduce the overhead of passing this data to the worker thread
|
||||||
const strippedMempool: { [txid: string]: ThreadTransaction } = {};
|
const strippedMempool: { [txid: string]: ThreadTransaction } = {};
|
||||||
@ -184,19 +188,21 @@ class MempoolBlocks {
|
|||||||
this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool });
|
this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool });
|
||||||
const { blocks, clusters } = await workerResultPromise;
|
const { blocks, clusters } = await workerResultPromise;
|
||||||
|
|
||||||
this.processBlockTemplates(newMempool, blocks, clusters);
|
|
||||||
|
|
||||||
// clean up thread error listener
|
// clean up thread error listener
|
||||||
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
||||||
|
|
||||||
|
return this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
logger.err('makeBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
|
return this.mempoolBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: string[]): Promise<void> {
|
public async updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: string[], saveResults: boolean = false): Promise<void> {
|
||||||
if (!this.txSelectionWorker) {
|
if (!this.txSelectionWorker) {
|
||||||
// need to reset the worker
|
// need to reset the worker
|
||||||
return this.makeBlockTemplates(newMempool);
|
this.makeBlockTemplates(newMempool, saveResults);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// prepare a stripped down version of the mempool with only the minimum necessary data
|
// prepare a stripped down version of the mempool with only the minimum necessary data
|
||||||
// to reduce the overhead of passing this data to the worker thread
|
// to reduce the overhead of passing this data to the worker thread
|
||||||
@ -224,16 +230,16 @@ class MempoolBlocks {
|
|||||||
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed });
|
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed });
|
||||||
const { blocks, clusters } = await workerResultPromise;
|
const { blocks, clusters } = await workerResultPromise;
|
||||||
|
|
||||||
this.processBlockTemplates(newMempool, blocks, clusters);
|
|
||||||
|
|
||||||
// clean up thread error listener
|
// clean up thread error listener
|
||||||
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
||||||
|
|
||||||
|
this.processBlockTemplates(newMempool, blocks, clusters, saveResults);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private processBlockTemplates(mempool, blocks, clusters): void {
|
private processBlockTemplates(mempool, blocks, clusters, saveResults): MempoolBlockWithTransactions[] {
|
||||||
// update this thread's mempool with the results
|
// update this thread's mempool with the results
|
||||||
blocks.forEach(block => {
|
blocks.forEach(block => {
|
||||||
block.forEach(tx => {
|
block.forEach(tx => {
|
||||||
@ -278,10 +284,13 @@ class MempoolBlocks {
|
|||||||
}).filter(tx => !!tx), undefined, undefined, blockIndex);
|
}).filter(tx => !!tx), undefined, undefined, blockIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
|
if (saveResults) {
|
||||||
|
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
|
||||||
|
this.mempoolBlocks = mempoolBlocks;
|
||||||
|
this.mempoolBlockDeltas = deltas;
|
||||||
|
}
|
||||||
|
|
||||||
this.mempoolBlocks = mempoolBlocks;
|
return mempoolBlocks;
|
||||||
this.mempoolBlockDeltas = deltas;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private dataToMempoolBlocks(transactions: TransactionExtended[],
|
private dataToMempoolBlocks(transactions: TransactionExtended[],
|
||||||
|
@ -210,7 +210,7 @@ class Mempool {
|
|||||||
for (const rbfTransaction in rbfTransactions) {
|
for (const rbfTransaction in rbfTransactions) {
|
||||||
if (this.mempoolCache[rbfTransaction]) {
|
if (this.mempoolCache[rbfTransaction]) {
|
||||||
// Store replaced transactions
|
// Store replaced transactions
|
||||||
rbfCache.add(rbfTransaction, rbfTransactions[rbfTransaction].txid);
|
rbfCache.add(this.mempoolCache[rbfTransaction], rbfTransactions[rbfTransaction].txid);
|
||||||
// Erase the replaced transactions from the local mempool
|
// Erase the replaced transactions from the local mempool
|
||||||
delete this.mempoolCache[rbfTransaction];
|
delete this.mempoolCache[rbfTransaction];
|
||||||
}
|
}
|
||||||
@ -236,6 +236,7 @@ class Mempool {
|
|||||||
const lazyDeleteAt = this.mempoolCache[tx].deleteAfter;
|
const lazyDeleteAt = this.mempoolCache[tx].deleteAfter;
|
||||||
if (lazyDeleteAt && lazyDeleteAt < now) {
|
if (lazyDeleteAt && lazyDeleteAt < now) {
|
||||||
delete this.mempoolCache[tx];
|
delete this.mempoolCache[tx];
|
||||||
|
rbfCache.evict(tx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -265,9 +265,9 @@ class Mining {
|
|||||||
}
|
}
|
||||||
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate());
|
await HashratesRepository.$setLatestRun('last_weekly_hashrates_indexing', new Date().getUTCDate());
|
||||||
if (newlyIndexed > 0) {
|
if (newlyIndexed > 0) {
|
||||||
logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`);
|
logger.notice(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`);
|
logger.debug(`Weekly mining pools hashrates indexing completed: indexed ${newlyIndexed}`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
|
loadingIndicators.setProgress('weekly-hashrate-indexing', 100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -370,14 +370,14 @@ class Mining {
|
|||||||
|
|
||||||
await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate());
|
await HashratesRepository.$setLatestRun('last_hashrates_indexing', new Date().getUTCDate());
|
||||||
if (newlyIndexed > 0) {
|
if (newlyIndexed > 0) {
|
||||||
logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`);
|
logger.notice(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.mining);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`);
|
logger.debug(`Daily network hashrate indexing completed: indexed ${newlyIndexed} days`, logger.tags.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)}`);
|
logger.err(`Daily network hashrate indexing failed. Trying again in 10 seconds. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -449,9 +449,9 @@ class Mining {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (totalIndexed > 0) {
|
if (totalIndexed > 0) {
|
||||||
logger.notice(`Indexed ${totalIndexed} difficulty adjustments`);
|
logger.notice(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Indexed ${totalIndexed} difficulty adjustments`);
|
logger.debug(`Indexed ${totalIndexed} difficulty adjustments`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ class PoolsParser {
|
|||||||
poolNames.push(poolsDuplicated[i].name);
|
poolNames.push(poolsDuplicated[i].name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.debug(`Found ${poolNames.length} unique mining pools`);
|
logger.debug(`Found ${poolNames.length} unique mining pools`, logger.tags.mining);
|
||||||
|
|
||||||
// Get existing pools from the db
|
// Get existing pools from the db
|
||||||
let existingPools;
|
let existingPools;
|
||||||
@ -72,7 +72,7 @@ class PoolsParser {
|
|||||||
existingPools = [];
|
existingPools = [];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot get existing pools from the database, skipping pools.json import');
|
logger.err('Cannot get existing pools from the database, skipping pools.json import', logger.tags.mining);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ class PoolsParser {
|
|||||||
slug = poolsJson['slugs'][poolNames[i]];
|
slug = poolsJson['slugs'][poolNames[i]];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (this.slugWarnFlag === false) {
|
if (this.slugWarnFlag === false) {
|
||||||
logger.warn(`pools.json does not seem to contain the 'slugs' object`);
|
logger.warn(`pools.json does not seem to contain the 'slugs' object`, logger.tags.mining);
|
||||||
this.slugWarnFlag = true;
|
this.slugWarnFlag = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,7 +107,7 @@ class PoolsParser {
|
|||||||
if (slug === undefined) {
|
if (slug === undefined) {
|
||||||
// Only keep alphanumerical
|
// Only keep alphanumerical
|
||||||
slug = poolNames[i].replace(/[^a-z0-9]/gi, '').toLowerCase();
|
slug = poolNames[i].replace(/[^a-z0-9]/gi, '').toLowerCase();
|
||||||
logger.warn(`No slug found for '${poolNames[i]}', generating it => '${slug}'`);
|
logger.warn(`No slug found for '${poolNames[i]}', generating it => '${slug}'`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
|
|
||||||
const poolObj = {
|
const poolObj = {
|
||||||
@ -127,7 +127,7 @@ class PoolsParser {
|
|||||||
if (!equals(JSON.parse(existingPool.addresses), poolObj.addresses) || !equals(JSON.parse(existingPool.regexes), poolObj.regexes)) {
|
if (!equals(JSON.parse(existingPool.addresses), poolObj.addresses) || !equals(JSON.parse(existingPool.regexes), poolObj.regexes)) {
|
||||||
finalPoolDataUpdate.push(poolObj);
|
finalPoolDataUpdate.push(poolObj);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (config.DATABASE.ENABLED) {
|
||||||
// Double check that if we're not just renaming a pool (same address same regex)
|
// Double check that if we're not just renaming a pool (same address same regex)
|
||||||
const [poolToRename]: any[] = await DB.query(`
|
const [poolToRename]: any[] = await DB.query(`
|
||||||
SELECT * FROM pools
|
SELECT * FROM pools
|
||||||
@ -143,9 +143,9 @@ class PoolsParser {
|
|||||||
'addresses': allAddresses,
|
'addresses': allAddresses,
|
||||||
'slug': slug
|
'slug': slug
|
||||||
});
|
});
|
||||||
logger.debug(`Rename '${poolToRename[0].name}' mining pool to ${poolObj.name}`);
|
logger.debug(`Rename '${poolToRename[0].name}' mining pool to ${poolObj.name}`, logger.tags.mining);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Add '${finalPoolName}' mining pool`);
|
logger.debug(`Add '${finalPoolName}' mining pool`, logger.tags.mining);
|
||||||
finalPoolDataAdd.push(poolObj);
|
finalPoolDataAdd.push(poolObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -160,14 +160,14 @@ class PoolsParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.DATABASE.ENABLED === false) { // Don't run db operations
|
if (config.DATABASE.ENABLED === false) { // Don't run db operations
|
||||||
logger.info('Mining pools.json import completed (no database)');
|
logger.info('Mining pools.json import completed (no database)', logger.tags.mining);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finalPoolDataAdd.length > 0 || finalPoolDataUpdate.length > 0 ||
|
if (finalPoolDataAdd.length > 0 || finalPoolDataUpdate.length > 0 ||
|
||||||
finalPoolDataRename.length > 0
|
finalPoolDataRename.length > 0
|
||||||
) {
|
) {
|
||||||
logger.debug(`Update pools table now`);
|
logger.debug(`Update pools table now`, logger.tags.mining);
|
||||||
|
|
||||||
// Add new mining pools into the database
|
// Add new mining pools into the database
|
||||||
let queryAdd: string = 'INSERT INTO pools(name, link, regexes, addresses, slug) VALUES ';
|
let queryAdd: string = 'INSERT INTO pools(name, link, regexes, addresses, slug) VALUES ';
|
||||||
@ -217,9 +217,9 @@ class PoolsParser {
|
|||||||
await DB.query({ sql: query, timeout: 120000 });
|
await DB.query({ sql: query, timeout: 120000 });
|
||||||
}
|
}
|
||||||
await this.insertUnknownPool();
|
await this.insertUnknownPool();
|
||||||
logger.info('Mining pools.json import completed');
|
logger.info('Mining pools.json import completed', logger.tags.mining);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot import pools in the database`);
|
logger.err(`Cannot import pools in the database`, logger.tags.mining);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -227,7 +227,7 @@ class PoolsParser {
|
|||||||
try {
|
try {
|
||||||
await this.insertUnknownPool();
|
await this.insertUnknownPool();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot insert unknown pool in the database`);
|
logger.err(`Cannot insert unknown pool in the database`, logger.tags.mining);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,7 +252,7 @@ class PoolsParser {
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Unable to insert "Unknown" mining pool');
|
logger.err('Unable to insert "Unknown" mining pool', logger.tags.mining);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,17 +272,17 @@ class PoolsParser {
|
|||||||
for (const updatedPool of finalPoolDataUpdate) {
|
for (const updatedPool of finalPoolDataUpdate) {
|
||||||
const [pool]: any[] = await DB.query(`SELECT id, name from pools where slug = "${updatedPool.slug}"`);
|
const [pool]: any[] = await DB.query(`SELECT id, name from pools where slug = "${updatedPool.slug}"`);
|
||||||
if (pool.length > 0) {
|
if (pool.length > 0) {
|
||||||
logger.notice(`Deleting blocks from ${pool[0].name} mining pool for future re-indexing`);
|
logger.notice(`Deleting blocks from ${pool[0].name} mining pool for future re-indexing`, logger.tags.mining);
|
||||||
await DB.query(`DELETE FROM blocks WHERE pool_id = ${pool[0].id}`);
|
await DB.query(`DELETE FROM blocks WHERE pool_id = ${pool[0].id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore early days of Bitcoin as there were not mining pool yet
|
// Ignore early days of Bitcoin as there were not mining pool yet
|
||||||
logger.notice('Deleting blocks with unknown mining pool from height 130635 for future re-indexing');
|
logger.notice(`Deleting blocks with unknown mining pool from height 130635 for future re-indexing`, logger.tags.mining);
|
||||||
const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
|
const [unknownPool] = await DB.query(`SELECT id from pools where slug = "unknown"`);
|
||||||
await DB.query(`DELETE FROM blocks WHERE pool_id = ${unknownPool[0].id} AND height > 130635`);
|
await DB.query(`DELETE FROM blocks WHERE pool_id = ${unknownPool[0].id} AND height > 130635`);
|
||||||
|
|
||||||
logger.notice('Truncating hashrates for future re-indexing');
|
logger.notice(`Truncating hashrates for future re-indexing`, logger.tags.mining);
|
||||||
await DB.query(`DELETE FROM hashrates`);
|
await DB.query(`DELETE FROM hashrates`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,31 +1,62 @@
|
|||||||
export interface CachedRbf {
|
import { TransactionExtended } from "../mempool.interfaces";
|
||||||
txid: string;
|
|
||||||
expires: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
class RbfCache {
|
class RbfCache {
|
||||||
private cache: { [txid: string]: CachedRbf; } = {};
|
private replacedBy: { [txid: string]: string; } = {};
|
||||||
|
private replaces: { [txid: string]: string[] } = {};
|
||||||
|
private txs: { [txid: string]: TransactionExtended } = {};
|
||||||
|
private expiring: { [txid: string]: Date } = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
|
setInterval(this.cleanup.bind(this), 1000 * 60 * 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
public add(replacedTxId: string, newTxId: string): void {
|
public add(replacedTx: TransactionExtended, newTxId: string): void {
|
||||||
this.cache[replacedTxId] = {
|
this.replacedBy[replacedTx.txid] = newTxId;
|
||||||
expires: new Date(Date.now() + 1000 * 604800), // 1 week
|
this.txs[replacedTx.txid] = replacedTx;
|
||||||
txid: newTxId,
|
if (!this.replaces[newTxId]) {
|
||||||
};
|
this.replaces[newTxId] = [];
|
||||||
|
}
|
||||||
|
this.replaces[newTxId].push(replacedTx.txid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(txId: string): CachedRbf | undefined {
|
public getReplacedBy(txId: string): string | undefined {
|
||||||
return this.cache[txId];
|
return this.replacedBy[txId];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getReplaces(txId: string): string[] | undefined {
|
||||||
|
return this.replaces[txId];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTx(txId: string): TransactionExtended | undefined {
|
||||||
|
return this.txs[txId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// flag a transaction as removed from the mempool
|
||||||
|
public evict(txid): void {
|
||||||
|
this.expiring[txid] = new Date(Date.now() + 1000 * 86400); // 24 hours
|
||||||
}
|
}
|
||||||
|
|
||||||
private cleanup(): void {
|
private cleanup(): void {
|
||||||
const currentDate = new Date();
|
const currentDate = new Date();
|
||||||
for (const c in this.cache) {
|
for (const txid in this.expiring) {
|
||||||
if (this.cache[c].expires < currentDate) {
|
if (this.expiring[txid] < currentDate) {
|
||||||
delete this.cache[c];
|
delete this.expiring[txid];
|
||||||
|
this.remove(txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove a transaction & all previous versions from the cache
|
||||||
|
private remove(txid): void {
|
||||||
|
// don't remove a transaction while a newer version remains in the mempool
|
||||||
|
if (this.replaces[txid] && !this.replacedBy[txid]) {
|
||||||
|
const replaces = this.replaces[txid];
|
||||||
|
delete this.replaces[txid];
|
||||||
|
for (const tx of replaces) {
|
||||||
|
// recursively remove prior versions from the cache
|
||||||
|
delete this.replacedBy[tx];
|
||||||
|
delete this.txs[tx];
|
||||||
|
this.remove(tx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
|
||||||
import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
||||||
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
||||||
import config from '../config';
|
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
|
import bitcoinApi, { bitcoinCoreApi } from './bitcoin/bitcoin-api-factory';
|
||||||
|
|
||||||
class TransactionUtils {
|
class TransactionUtils {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -21,8 +20,19 @@ class TransactionUtils {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false): Promise<TransactionExtended> {
|
/**
|
||||||
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
|
* @param txId
|
||||||
|
* @param addPrevouts
|
||||||
|
* @param lazyPrevouts
|
||||||
|
* @param forceCore - See https://github.com/mempool/mempool/issues/2904
|
||||||
|
*/
|
||||||
|
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false, forceCore = false): Promise<TransactionExtended> {
|
||||||
|
let transaction: IEsploraApi.Transaction;
|
||||||
|
if (forceCore === true) {
|
||||||
|
transaction = await bitcoinCoreApi.$getRawTransaction(txId, true);
|
||||||
|
} else {
|
||||||
|
transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
|
||||||
|
}
|
||||||
return this.extendTransaction(transaction);
|
return this.extendTransaction(transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import blocks from './blocks';
|
|||||||
import memPool from './mempool';
|
import memPool from './mempool';
|
||||||
import backendInfo from './backend-info';
|
import backendInfo from './backend-info';
|
||||||
import mempoolBlocks from './mempool-blocks';
|
import mempoolBlocks from './mempool-blocks';
|
||||||
import fiatConversion from './fiat-conversion';
|
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
import loadingIndicators from './loading-indicators';
|
import loadingIndicators from './loading-indicators';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
@ -19,6 +18,8 @@ import feeApi from './fee-api';
|
|||||||
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||||
import Audit from './audit';
|
import Audit from './audit';
|
||||||
|
import { deepClone } from '../utils/clone';
|
||||||
|
import priceUpdater from '../tasks/price-updater';
|
||||||
|
|
||||||
class WebsocketHandler {
|
class WebsocketHandler {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@ -58,10 +59,10 @@ class WebsocketHandler {
|
|||||||
client['track-tx'] = parsedMessage['track-tx'];
|
client['track-tx'] = parsedMessage['track-tx'];
|
||||||
// Client is telling the transaction wasn't found
|
// Client is telling the transaction wasn't found
|
||||||
if (parsedMessage['watch-mempool']) {
|
if (parsedMessage['watch-mempool']) {
|
||||||
const rbfCacheTx = rbfCache.get(client['track-tx']);
|
const rbfCacheTxid = rbfCache.getReplacedBy(client['track-tx']);
|
||||||
if (rbfCacheTx) {
|
if (rbfCacheTxid) {
|
||||||
response['txReplaced'] = {
|
response['txReplaced'] = {
|
||||||
txid: rbfCacheTx.txid,
|
txid: rbfCacheTxid,
|
||||||
};
|
};
|
||||||
client['track-tx'] = null;
|
client['track-tx'] = null;
|
||||||
} else {
|
} else {
|
||||||
@ -213,7 +214,7 @@ class WebsocketHandler {
|
|||||||
'mempoolInfo': memPool.getMempoolInfo(),
|
'mempoolInfo': memPool.getMempoolInfo(),
|
||||||
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
'vBytesPerSecond': memPool.getVBytesPerSecond(),
|
||||||
'blocks': _blocks,
|
'blocks': _blocks,
|
||||||
'conversions': fiatConversion.getConversionRates(),
|
'conversions': priceUpdater.latestPrices,
|
||||||
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
|
||||||
'transactions': memPool.getLatestTransactions(),
|
'transactions': memPool.getLatestTransactions(),
|
||||||
'backendInfo': backendInfo.getBackendInfo(),
|
'backendInfo': backendInfo.getBackendInfo(),
|
||||||
@ -251,9 +252,9 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||||
await mempoolBlocks.updateBlockTemplates(newMempool, newTransactions, deletedTransactions.map(tx => tx.txid));
|
await mempoolBlocks.updateBlockTemplates(newMempool, newTransactions, deletedTransactions.map(tx => tx.txid), true);
|
||||||
} else {
|
} else {
|
||||||
mempoolBlocks.updateMempoolBlocks(newMempool);
|
mempoolBlocks.updateMempoolBlocks(newMempool, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||||
@ -418,47 +419,51 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
const _memPool = memPool.getMempool();
|
const _memPool = memPool.getMempool();
|
||||||
|
|
||||||
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
|
if (config.MEMPOOL.AUDIT) {
|
||||||
await mempoolBlocks.makeBlockTemplates(_memPool);
|
let projectedBlocks;
|
||||||
} else {
|
// template calculation functions have mempool side effects, so calculate audits using
|
||||||
mempoolBlocks.updateMempoolBlocks(_memPool);
|
// a cloned copy of the mempool if we're running a different algorithm for mempool updates
|
||||||
}
|
const auditMempool = (config.MEMPOOL.ADVANCED_GBT_AUDIT === config.MEMPOOL.ADVANCED_GBT_MEMPOOL) ? _memPool : deepClone(_memPool);
|
||||||
|
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
|
||||||
|
projectedBlocks = await mempoolBlocks.makeBlockTemplates(auditMempool, false);
|
||||||
|
} else {
|
||||||
|
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
|
||||||
|
}
|
||||||
|
|
||||||
if (Common.indexingEnabled() && memPool.isInSync()) {
|
if (Common.indexingEnabled() && memPool.isInSync()) {
|
||||||
const projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||||
|
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||||
|
|
||||||
const { censored, added, fresh, score } = Audit.auditBlock(transactions, projectedBlocks, _memPool);
|
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
|
||||||
const matchRate = Math.round(score * 100 * 100) / 100;
|
return {
|
||||||
|
txid: tx.txid,
|
||||||
|
vsize: tx.vsize,
|
||||||
|
fee: tx.fee ? Math.round(tx.fee) : 0,
|
||||||
|
value: tx.value,
|
||||||
|
};
|
||||||
|
}) : [];
|
||||||
|
|
||||||
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions.map((tx) => {
|
BlocksSummariesRepository.$saveTemplate({
|
||||||
return {
|
height: block.height,
|
||||||
txid: tx.txid,
|
template: {
|
||||||
vsize: tx.vsize,
|
id: block.id,
|
||||||
fee: tx.fee ? Math.round(tx.fee) : 0,
|
transactions: stripped
|
||||||
value: tx.value,
|
}
|
||||||
};
|
});
|
||||||
}) : [];
|
|
||||||
|
|
||||||
BlocksSummariesRepository.$saveTemplate({
|
BlocksAuditsRepository.$saveAudit({
|
||||||
height: block.height,
|
time: block.timestamp,
|
||||||
template: {
|
height: block.height,
|
||||||
id: block.id,
|
hash: block.id,
|
||||||
transactions: stripped
|
addedTxs: added,
|
||||||
|
missingTxs: censored,
|
||||||
|
freshTxs: fresh,
|
||||||
|
matchRate: matchRate,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (block.extras) {
|
||||||
|
block.extras.matchRate = matchRate;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
BlocksAuditsRepository.$saveAudit({
|
|
||||||
time: block.timestamp,
|
|
||||||
height: block.height,
|
|
||||||
hash: block.id,
|
|
||||||
addedTxs: added,
|
|
||||||
missingTxs: censored,
|
|
||||||
freshTxs: fresh,
|
|
||||||
matchRate: matchRate,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (block.extras) {
|
|
||||||
block.extras.matchRate = matchRate;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -467,12 +472,13 @@ class WebsocketHandler {
|
|||||||
for (const txId of txIds) {
|
for (const txId of txIds) {
|
||||||
delete _memPool[txId];
|
delete _memPool[txId];
|
||||||
removed.push(txId);
|
removed.push(txId);
|
||||||
|
rbfCache.evict(txId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||||
await mempoolBlocks.updateBlockTemplates(_memPool, [], removed);
|
await mempoolBlocks.updateBlockTemplates(_memPool, [], removed, true);
|
||||||
} else {
|
} else {
|
||||||
mempoolBlocks.updateMempoolBlocks(_memPool);
|
mempoolBlocks.updateMempoolBlocks(_memPool, true);
|
||||||
}
|
}
|
||||||
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
const mBlocks = mempoolBlocks.getMempoolBlocks();
|
||||||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||||
|
@ -29,9 +29,10 @@ interface IConfig {
|
|||||||
AUTOMATIC_BLOCK_REINDEXING: boolean;
|
AUTOMATIC_BLOCK_REINDEXING: boolean;
|
||||||
POOLS_JSON_URL: string,
|
POOLS_JSON_URL: string,
|
||||||
POOLS_JSON_TREE_URL: string,
|
POOLS_JSON_TREE_URL: string,
|
||||||
|
AUDIT: boolean;
|
||||||
ADVANCED_GBT_AUDIT: boolean;
|
ADVANCED_GBT_AUDIT: boolean;
|
||||||
ADVANCED_GBT_MEMPOOL: boolean;
|
ADVANCED_GBT_MEMPOOL: boolean;
|
||||||
TRANSACTION_INDEXING: boolean;
|
CPFP_INDEXING: boolean;
|
||||||
};
|
};
|
||||||
ESPLORA: {
|
ESPLORA: {
|
||||||
REST_API_URL: string;
|
REST_API_URL: string;
|
||||||
@ -150,9 +151,10 @@ const defaults: IConfig = {
|
|||||||
'AUTOMATIC_BLOCK_REINDEXING': false,
|
'AUTOMATIC_BLOCK_REINDEXING': false,
|
||||||
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
|
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json',
|
||||||
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||||
|
'AUDIT': false,
|
||||||
'ADVANCED_GBT_AUDIT': false,
|
'ADVANCED_GBT_AUDIT': false,
|
||||||
'ADVANCED_GBT_MEMPOOL': false,
|
'ADVANCED_GBT_MEMPOOL': false,
|
||||||
'TRANSACTION_INDEXING': false,
|
'CPFP_INDEXING': false,
|
||||||
},
|
},
|
||||||
'ESPLORA': {
|
'ESPLORA': {
|
||||||
'REST_API_URL': 'http://127.0.0.1:3000',
|
'REST_API_URL': 'http://127.0.0.1:3000',
|
||||||
|
@ -10,7 +10,6 @@ import memPool from './api/mempool';
|
|||||||
import diskCache from './api/disk-cache';
|
import diskCache from './api/disk-cache';
|
||||||
import statistics from './api/statistics/statistics';
|
import statistics from './api/statistics/statistics';
|
||||||
import websocketHandler from './api/websocket-handler';
|
import websocketHandler from './api/websocket-handler';
|
||||||
import fiatConversion from './api/fiat-conversion';
|
|
||||||
import bisq from './api/bisq/bisq';
|
import bisq from './api/bisq/bisq';
|
||||||
import bisqMarkets from './api/bisq/markets';
|
import bisqMarkets from './api/bisq/markets';
|
||||||
import logger from './logger';
|
import logger from './logger';
|
||||||
@ -36,6 +35,7 @@ import liquidRoutes from './api/liquid/liquid.routes';
|
|||||||
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
|
import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
|
||||||
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
|
import fundingTxFetcher from './tasks/lightning/sync-tasks/funding-tx-fetcher';
|
||||||
import forensicsService from './tasks/lightning/forensics.service';
|
import forensicsService from './tasks/lightning/forensics.service';
|
||||||
|
import priceUpdater from './tasks/price-updater';
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@ -78,25 +78,6 @@ class Server {
|
|||||||
async startServer(worker = false): Promise<void> {
|
async startServer(worker = false): Promise<void> {
|
||||||
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
|
logger.notice(`Starting Mempool Server${worker ? ' (worker)' : ''}... (${backendInfo.getShortCommitHash()})`);
|
||||||
|
|
||||||
this.app
|
|
||||||
.use((req: Request, res: Response, next: NextFunction) => {
|
|
||||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
||||||
next();
|
|
||||||
})
|
|
||||||
.use(express.urlencoded({ extended: true }))
|
|
||||||
.use(express.text({ type: ['text/plain', 'application/base64'] }))
|
|
||||||
;
|
|
||||||
|
|
||||||
this.server = http.createServer(this.app);
|
|
||||||
this.wss = new WebSocket.Server({ server: this.server });
|
|
||||||
|
|
||||||
this.setUpWebsocketHandling();
|
|
||||||
|
|
||||||
await syncAssets.syncAssets$();
|
|
||||||
if (config.MEMPOOL.ENABLED) {
|
|
||||||
diskCache.loadMempoolCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.DATABASE.ENABLED) {
|
if (config.DATABASE.ENABLED) {
|
||||||
await DB.checkDbConnection();
|
await DB.checkDbConnection();
|
||||||
try {
|
try {
|
||||||
@ -115,6 +96,29 @@ class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.app
|
||||||
|
.use((req: Request, res: Response, next: NextFunction) => {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
next();
|
||||||
|
})
|
||||||
|
.use(express.urlencoded({ extended: true }))
|
||||||
|
.use(express.text({ type: ['text/plain', 'application/base64'] }))
|
||||||
|
;
|
||||||
|
|
||||||
|
if (config.DATABASE.ENABLED) {
|
||||||
|
await priceUpdater.$initializeLatestPriceWithDb();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.server = http.createServer(this.app);
|
||||||
|
this.wss = new WebSocket.Server({ server: this.server });
|
||||||
|
|
||||||
|
this.setUpWebsocketHandling();
|
||||||
|
|
||||||
|
await syncAssets.syncAssets$();
|
||||||
|
if (config.MEMPOOL.ENABLED) {
|
||||||
|
diskCache.loadMempoolCache();
|
||||||
|
}
|
||||||
|
|
||||||
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
|
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && cluster.isPrimary) {
|
||||||
statistics.startStatistics();
|
statistics.startStatistics();
|
||||||
}
|
}
|
||||||
@ -127,7 +131,7 @@ class Server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fiatConversion.startService();
|
priceUpdater.$run();
|
||||||
|
|
||||||
this.setUpHttpApiRoutes();
|
this.setUpHttpApiRoutes();
|
||||||
|
|
||||||
@ -221,7 +225,7 @@ class Server {
|
|||||||
memPool.setAsyncMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
|
memPool.setAsyncMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
|
||||||
blocks.setNewAsyncBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
|
blocks.setNewAsyncBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
|
||||||
}
|
}
|
||||||
fiatConversion.setProgressChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
||||||
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,22 +32,27 @@ class Logger {
|
|||||||
local7: 23
|
local7: 23
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public tags = {
|
||||||
|
mining: 'Mining',
|
||||||
|
ln: 'Lightning',
|
||||||
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
public emerg: ((msg: string) => void);
|
public emerg: ((msg: string, tag?: string) => void);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
public alert: ((msg: string) => void);
|
public alert: ((msg: string, tag?: string) => void);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
public crit: ((msg: string) => void);
|
public crit: ((msg: string, tag?: string) => void);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
public err: ((msg: string) => void);
|
public err: ((msg: string, tag?: string) => void);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
public warn: ((msg: string) => void);
|
public warn: ((msg: string, tag?: string) => void);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
public notice: ((msg: string) => void);
|
public notice: ((msg: string, tag?: string) => void);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
public info: ((msg: string) => void);
|
public info: ((msg: string, tag?: string) => void);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
public debug: ((msg: string) => void);
|
public debug: ((msg: string, tag?: string) => void);
|
||||||
|
|
||||||
private name = 'mempool';
|
private name = 'mempool';
|
||||||
private client: dgram.Socket;
|
private client: dgram.Socket;
|
||||||
@ -66,8 +71,8 @@ class Logger {
|
|||||||
|
|
||||||
private addprio(prio): void {
|
private addprio(prio): void {
|
||||||
this[prio] = (function(_this) {
|
this[prio] = (function(_this) {
|
||||||
return function(msg) {
|
return function(msg, tag?: string) {
|
||||||
return _this.msg(prio, msg);
|
return _this.msg(prio, msg, tag);
|
||||||
};
|
};
|
||||||
})(this);
|
})(this);
|
||||||
}
|
}
|
||||||
@ -85,7 +90,7 @@ class Logger {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
private msg(priority, msg) {
|
private msg(priority, msg, tag?: string) {
|
||||||
let consolemsg, prionum, syslogmsg;
|
let consolemsg, prionum, syslogmsg;
|
||||||
if (typeof msg === 'string' && msg.length > 0) {
|
if (typeof msg === 'string' && msg.length > 0) {
|
||||||
while (msg[msg.length - 1].charCodeAt(0) === 10) {
|
while (msg[msg.length - 1].charCodeAt(0) === 10) {
|
||||||
@ -94,10 +99,10 @@ class Logger {
|
|||||||
}
|
}
|
||||||
const network = this.network ? ' <' + this.network + '>' : '';
|
const network = this.network ? ' <' + this.network + '>' : '';
|
||||||
prionum = Logger.priorities[priority] || Logger.priorities.info;
|
prionum = Logger.priorities[priority] || Logger.priorities.info;
|
||||||
consolemsg = `${this.ts()} [${process.pid}] ${priority.toUpperCase()}:${network} ${msg}`;
|
consolemsg = `${this.ts()} [${process.pid}] ${priority.toUpperCase()}:${network} ${tag ? '[' + tag + '] ' : ''}${msg}`;
|
||||||
|
|
||||||
if (config.SYSLOG.ENABLED && Logger.priorities[priority] <= Logger.priorities[config.SYSLOG.MIN_PRIORITY]) {
|
if (config.SYSLOG.ENABLED && Logger.priorities[priority] <= Logger.priorities[config.SYSLOG.MIN_PRIORITY]) {
|
||||||
syslogmsg = `<${(Logger.facilities[config.SYSLOG.FACILITY] * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${msg}`;
|
syslogmsg = `<${(Logger.facilities[config.SYSLOG.FACILITY] * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${tag ? '[' + tag + '] ' : ''}${msg}`;
|
||||||
this.syslog(syslogmsg);
|
this.syslog(syslogmsg);
|
||||||
}
|
}
|
||||||
if (Logger.priorities[priority] > Logger.priorities[config.MEMPOOL.STDOUT_LOG_MIN_PRIORITY]) {
|
if (Logger.priorities[priority] > Logger.priorities[config.MEMPOOL.STDOUT_LOG_MIN_PRIORITY]) {
|
||||||
|
@ -274,6 +274,7 @@ export interface IBackendInfo {
|
|||||||
hostname: string;
|
hostname: string;
|
||||||
gitCommit: string;
|
gitCommit: string;
|
||||||
version: string;
|
version: string;
|
||||||
|
lightning: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDifficultyAdjustment {
|
export interface IDifficultyAdjustment {
|
||||||
@ -337,4 +338,4 @@ export interface IOldestNodes {
|
|||||||
updatedAt?: number,
|
updatedAt?: number,
|
||||||
city?: any,
|
city?: any,
|
||||||
country?: any,
|
country?: any,
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ import HashratesRepository from './HashratesRepository';
|
|||||||
import { escape } from 'mysql2';
|
import { escape } from 'mysql2';
|
||||||
import BlocksSummariesRepository from './BlocksSummariesRepository';
|
import BlocksSummariesRepository from './BlocksSummariesRepository';
|
||||||
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
|
import DifficultyAdjustmentsRepository from './DifficultyAdjustmentsRepository';
|
||||||
|
import bitcoinClient from '../api/bitcoin/bitcoin-client';
|
||||||
|
import config from '../config';
|
||||||
|
|
||||||
class BlocksRepository {
|
class BlocksRepository {
|
||||||
/**
|
/**
|
||||||
@ -519,7 +521,7 @@ class BlocksRepository {
|
|||||||
CAST(AVG(blocks.height) as INT) as avgHeight,
|
CAST(AVG(blocks.height) as INT) as avgHeight,
|
||||||
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
|
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
|
||||||
CAST(AVG(fees) as INT) as avgFees,
|
CAST(AVG(fees) as INT) as avgFees,
|
||||||
prices.USD
|
prices.*
|
||||||
FROM blocks
|
FROM blocks
|
||||||
JOIN blocks_prices on blocks_prices.height = blocks.height
|
JOIN blocks_prices on blocks_prices.height = blocks.height
|
||||||
JOIN prices on prices.id = blocks_prices.price_id
|
JOIN prices on prices.id = blocks_prices.price_id
|
||||||
@ -548,7 +550,7 @@ class BlocksRepository {
|
|||||||
CAST(AVG(blocks.height) as INT) as avgHeight,
|
CAST(AVG(blocks.height) as INT) as avgHeight,
|
||||||
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
|
CAST(AVG(UNIX_TIMESTAMP(blockTimestamp)) as INT) as timestamp,
|
||||||
CAST(AVG(reward) as INT) as avgRewards,
|
CAST(AVG(reward) as INT) as avgRewards,
|
||||||
prices.USD
|
prices.*
|
||||||
FROM blocks
|
FROM blocks
|
||||||
JOIN blocks_prices on blocks_prices.height = blocks.height
|
JOIN blocks_prices on blocks_prices.height = blocks.height
|
||||||
JOIN prices on prices.id = blocks_prices.price_id
|
JOIN prices on prices.id = blocks_prices.price_id
|
||||||
@ -667,16 +669,32 @@ class BlocksRepository {
|
|||||||
*/
|
*/
|
||||||
public async $getCPFPUnindexedBlocks(): Promise<any[]> {
|
public async $getCPFPUnindexedBlocks(): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
const [rows]: any = await DB.query(`SELECT height, hash FROM blocks WHERE cpfp_indexed = 0 ORDER BY height DESC`);
|
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
|
||||||
return rows;
|
const currentBlockHeight = blockchainInfo.blocks;
|
||||||
|
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight);
|
||||||
|
if (indexingBlockAmount <= -1) {
|
||||||
|
indexingBlockAmount = currentBlockHeight + 1;
|
||||||
|
}
|
||||||
|
const minHeight = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
|
||||||
|
|
||||||
|
const [rows]: any[] = await DB.query(`
|
||||||
|
SELECT height
|
||||||
|
FROM compact_cpfp_clusters
|
||||||
|
WHERE height <= ? AND height >= ?
|
||||||
|
ORDER BY height DESC;
|
||||||
|
`, [currentBlockHeight, minHeight]);
|
||||||
|
|
||||||
|
const indexedHeights = {};
|
||||||
|
rows.forEach((row) => { indexedHeights[row.height] = true; });
|
||||||
|
const allHeights: number[] = Array.from(Array(currentBlockHeight - minHeight + 1).keys(), n => n + minHeight).reverse();
|
||||||
|
const unindexedHeights = allHeights.filter(x => !indexedHeights[x]);
|
||||||
|
|
||||||
|
return unindexedHeights;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot fetch CPFP unindexed blocks. Reason: ' + (e instanceof Error ? e.message : e));
|
logger.err('Cannot fetch CPFP unindexed blocks. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
return [];
|
||||||
|
|
||||||
public async $setCPFPIndexed(hash: string): Promise<void> {
|
|
||||||
await DB.query(`UPDATE blocks SET cpfp_indexed = 1 WHERE hash = ?`, [hash]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,34 +1,154 @@
|
|||||||
|
import cluster, { Cluster } from 'cluster';
|
||||||
|
import { RowDataPacket } from 'mysql2';
|
||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { Ancestor } from '../mempool.interfaces';
|
import { Ancestor } from '../mempool.interfaces';
|
||||||
|
import transactionRepository from '../repositories/TransactionRepository';
|
||||||
|
|
||||||
class CpfpRepository {
|
class CpfpRepository {
|
||||||
public async $saveCluster(height: number, txs: Ancestor[], effectiveFeePerVsize: number): Promise<void> {
|
public async $saveCluster(clusterRoot: string, height: number, txs: Ancestor[], effectiveFeePerVsize: number): Promise<boolean> {
|
||||||
|
if (!txs[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// skip clusters of transactions with the same fees
|
||||||
|
const roundedEffectiveFee = Math.round(effectiveFeePerVsize * 100) / 100;
|
||||||
|
const equalFee = txs.reduce((acc, tx) => {
|
||||||
|
return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee);
|
||||||
|
}, true);
|
||||||
|
if (equalFee) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const txsJson = JSON.stringify(txs);
|
const packedTxs = Buffer.from(this.pack(txs));
|
||||||
await DB.query(
|
await DB.query(
|
||||||
`
|
`
|
||||||
INSERT INTO cpfp_clusters(root, height, txs, fee_rate)
|
INSERT INTO compact_cpfp_clusters(root, height, txs, fee_rate)
|
||||||
VALUE (?, ?, ?, ?)
|
VALUE (UNHEX(?), ?, ?, ?)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
height = ?,
|
height = ?,
|
||||||
txs = ?,
|
txs = ?,
|
||||||
fee_rate = ?
|
fee_rate = ?
|
||||||
`,
|
`,
|
||||||
[txs[0].txid, height, txsJson, effectiveFeePerVsize, height, txsJson, effectiveFeePerVsize, height]
|
[clusterRoot, height, packedTxs, effectiveFeePerVsize, height, packedTxs, effectiveFeePerVsize]
|
||||||
);
|
);
|
||||||
|
const maxChunk = 10;
|
||||||
|
let chunkIndex = 0;
|
||||||
|
while (chunkIndex < txs.length) {
|
||||||
|
const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk).map(tx => {
|
||||||
|
return { txid: tx.txid, cluster: clusterRoot };
|
||||||
|
});
|
||||||
|
await transactionRepository.$batchSetCluster(chunk);
|
||||||
|
chunkIndex += maxChunk;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.err(`Cannot save cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Cannot save cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $batchSaveClusters(clusters: { root: string, height: number, txs: any, effectiveFeePerVsize: number}[]): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const clusterValues: any[] = [];
|
||||||
|
const txs: any[] = [];
|
||||||
|
|
||||||
|
for (const cluster of clusters) {
|
||||||
|
if (cluster.txs?.length > 1) {
|
||||||
|
const roundedEffectiveFee = Math.round(cluster.effectiveFeePerVsize * 100) / 100;
|
||||||
|
const equalFee = cluster.txs.reduce((acc, tx) => {
|
||||||
|
return (acc && Math.round(((tx.fee || 0) / (tx.weight / 4)) * 100) / 100 === roundedEffectiveFee);
|
||||||
|
}, true);
|
||||||
|
if (!equalFee) {
|
||||||
|
clusterValues.push([
|
||||||
|
cluster.root,
|
||||||
|
cluster.height,
|
||||||
|
Buffer.from(this.pack(cluster.txs)),
|
||||||
|
cluster.effectiveFeePerVsize
|
||||||
|
]);
|
||||||
|
for (const tx of cluster.txs) {
|
||||||
|
txs.push({ txid: tx.txid, cluster: cluster.root });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clusterValues.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxChunk = 100;
|
||||||
|
let chunkIndex = 0;
|
||||||
|
// insert transactions in batches of up to 100 rows
|
||||||
|
while (chunkIndex < txs.length) {
|
||||||
|
const chunk = txs.slice(chunkIndex, chunkIndex + maxChunk);
|
||||||
|
await transactionRepository.$batchSetCluster(chunk);
|
||||||
|
chunkIndex += maxChunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkIndex = 0;
|
||||||
|
// insert clusters in batches of up to 100 rows
|
||||||
|
while (chunkIndex < clusterValues.length) {
|
||||||
|
const chunk = clusterValues.slice(chunkIndex, chunkIndex + maxChunk);
|
||||||
|
let query = `
|
||||||
|
INSERT IGNORE INTO compact_cpfp_clusters(root, height, txs, fee_rate)
|
||||||
|
VALUES
|
||||||
|
`;
|
||||||
|
query += chunk.map(chunk => {
|
||||||
|
return (' (UNHEX(?), ?, ?, ?)');
|
||||||
|
}) + ';';
|
||||||
|
const values = chunk.flat();
|
||||||
|
await DB.query(
|
||||||
|
query,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
chunkIndex += maxChunk;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot save cpfp clusters into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getCluster(clusterRoot: string): Promise<Cluster | void> {
|
||||||
|
const [clusterRows]: any = await DB.query(
|
||||||
|
`
|
||||||
|
SELECT *
|
||||||
|
FROM compact_cpfp_clusters
|
||||||
|
WHERE root = UNHEX(?)
|
||||||
|
`,
|
||||||
|
[clusterRoot]
|
||||||
|
);
|
||||||
|
const cluster = clusterRows[0];
|
||||||
|
if (cluster?.txs) {
|
||||||
|
cluster.txs = this.unpack(cluster.txs);
|
||||||
|
return cluster;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
public async $deleteClustersFrom(height: number): Promise<void> {
|
public async $deleteClustersFrom(height: number): Promise<void> {
|
||||||
logger.info(`Delete newer cpfp clusters from height ${height} from the database`);
|
logger.info(`Delete newer cpfp clusters from height ${height} from the database`);
|
||||||
try {
|
try {
|
||||||
|
const [rows] = await DB.query(
|
||||||
|
`
|
||||||
|
SELECT txs, height, root from compact_cpfp_clusters
|
||||||
|
WHERE height >= ?
|
||||||
|
`,
|
||||||
|
[height]
|
||||||
|
) as RowDataPacket[][];
|
||||||
|
if (rows?.length) {
|
||||||
|
for (const clusterToDelete of rows) {
|
||||||
|
const txs = this.unpack(clusterToDelete?.txs);
|
||||||
|
for (const tx of txs) {
|
||||||
|
await transactionRepository.$removeTransaction(tx.txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
await DB.query(
|
await DB.query(
|
||||||
`
|
`
|
||||||
DELETE from cpfp_clusters
|
DELETE from compact_cpfp_clusters
|
||||||
WHERE height >= ?
|
WHERE height >= ?
|
||||||
`,
|
`,
|
||||||
[height]
|
[height]
|
||||||
@ -38,6 +158,75 @@ class CpfpRepository {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// insert a dummy row to mark that we've indexed as far as this block
|
||||||
|
public async $insertProgressMarker(height: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
const [rows]: any = await DB.query(
|
||||||
|
`
|
||||||
|
SELECT root
|
||||||
|
FROM compact_cpfp_clusters
|
||||||
|
WHERE height = ?
|
||||||
|
`,
|
||||||
|
[height]
|
||||||
|
);
|
||||||
|
if (!rows?.length) {
|
||||||
|
const rootBuffer = Buffer.alloc(32);
|
||||||
|
rootBuffer.writeInt32LE(height);
|
||||||
|
await DB.query(
|
||||||
|
`
|
||||||
|
INSERT INTO compact_cpfp_clusters(root, height, fee_rate)
|
||||||
|
VALUE (?, ?, ?)
|
||||||
|
`,
|
||||||
|
[rootBuffer, height, 0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot insert cpfp progress marker. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public pack(txs: Ancestor[]): ArrayBuffer {
|
||||||
|
const buf = new ArrayBuffer(44 * txs.length);
|
||||||
|
const view = new DataView(buf);
|
||||||
|
txs.forEach((tx, i) => {
|
||||||
|
const offset = i * 44;
|
||||||
|
for (let x = 0; x < 32; x++) {
|
||||||
|
// store txid in little-endian
|
||||||
|
view.setUint8(offset + (31 - x), parseInt(tx.txid.slice(x * 2, (x * 2) + 2), 16));
|
||||||
|
}
|
||||||
|
view.setUint32(offset + 32, tx.weight);
|
||||||
|
view.setBigUint64(offset + 36, BigInt(Math.round(tx.fee)));
|
||||||
|
});
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unpack(buf: Buffer): Ancestor[] {
|
||||||
|
if (!buf) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
||||||
|
const txs: Ancestor[] = [];
|
||||||
|
const view = new DataView(arrayBuffer);
|
||||||
|
for (let offset = 0; offset < arrayBuffer.byteLength; offset += 44) {
|
||||||
|
const txid = Array.from(new Uint8Array(arrayBuffer, offset, 32)).reverse().map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
const weight = view.getUint32(offset + 32);
|
||||||
|
const fee = Number(view.getBigUint64(offset + 36));
|
||||||
|
txs.push({
|
||||||
|
txid,
|
||||||
|
weight,
|
||||||
|
fee
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return txs;
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Failed to unpack CPFP cluster. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new CpfpRepository();
|
export default new CpfpRepository();
|
@ -1,12 +1,13 @@
|
|||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { Prices } from '../tasks/price-updater';
|
import { IConversionRates } from '../mempool.interfaces';
|
||||||
|
import priceUpdater from '../tasks/price-updater';
|
||||||
|
|
||||||
class PricesRepository {
|
class PricesRepository {
|
||||||
public async $savePrices(time: number, prices: Prices): Promise<void> {
|
public async $savePrices(time: number, prices: IConversionRates): Promise<void> {
|
||||||
if (prices.USD === -1) {
|
if (prices.USD === 0) {
|
||||||
// Some historical price entries have not USD prices, so we just ignore them to avoid future UX issues
|
// Some historical price entries have no USD prices, so we just ignore them to avoid future UX issues
|
||||||
// As of today there are only 4 (on 2013-09-05, 2013-09-19, 2013-09-12 and 2013-09-26) so that's fine
|
// As of today there are only 4 (on 2013-09-05, 2013-0909, 2013-09-12 and 2013-09-26) so that's fine
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,22 +24,22 @@ class PricesRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $getOldestPriceTime(): Promise<number> {
|
public async $getOldestPriceTime(): Promise<number> {
|
||||||
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time LIMIT 1`);
|
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time LIMIT 1`);
|
||||||
return oldestRow[0] ? oldestRow[0].time : 0;
|
return oldestRow[0] ? oldestRow[0].time : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getLatestPriceId(): Promise<number | null> {
|
public async $getLatestPriceId(): Promise<number | null> {
|
||||||
const [oldestRow] = await DB.query(`SELECT id from prices WHERE USD != -1 ORDER BY time DESC LIMIT 1`);
|
const [oldestRow] = await DB.query(`SELECT id from prices WHERE USD != 0 ORDER BY time DESC LIMIT 1`);
|
||||||
return oldestRow[0] ? oldestRow[0].id : null;
|
return oldestRow[0] ? oldestRow[0].id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getLatestPriceTime(): Promise<number> {
|
public async $getLatestPriceTime(): Promise<number> {
|
||||||
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time DESC LIMIT 1`);
|
const [oldestRow] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time DESC LIMIT 1`);
|
||||||
return oldestRow[0] ? oldestRow[0].time : 0;
|
return oldestRow[0] ? oldestRow[0].time : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getPricesTimes(): Promise<number[]> {
|
public async $getPricesTimes(): Promise<number[]> {
|
||||||
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != -1 ORDER BY time`);
|
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time from prices WHERE USD != 0 ORDER BY time`);
|
||||||
return times.map(time => time.time);
|
return times.map(time => time.time);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,6 +47,19 @@ class PricesRepository {
|
|||||||
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time, id, USD from prices ORDER BY time`);
|
const [times]: any[] = await DB.query(`SELECT UNIX_TIMESTAMP(time) as time, id, USD from prices ORDER BY time`);
|
||||||
return times;
|
return times;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getLatestConversionRates(): Promise<any> {
|
||||||
|
const [rates]: any[] = await DB.query(`
|
||||||
|
SELECT USD, EUR, GBP, CAD, CHF, AUD, JPY
|
||||||
|
FROM prices
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT 1`
|
||||||
|
);
|
||||||
|
if (!rates || rates.length === 0) {
|
||||||
|
return priceUpdater.getEmptyPricesObj();
|
||||||
|
}
|
||||||
|
return rates[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new PricesRepository();
|
export default new PricesRepository();
|
||||||
|
@ -1,31 +1,23 @@
|
|||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { Ancestor, CpfpInfo } from '../mempool.interfaces';
|
import { Ancestor, CpfpInfo } from '../mempool.interfaces';
|
||||||
|
import cpfpRepository from './CpfpRepository';
|
||||||
interface CpfpSummary {
|
|
||||||
txid: string;
|
|
||||||
cluster: string;
|
|
||||||
root: string;
|
|
||||||
txs: Ancestor[];
|
|
||||||
height: number;
|
|
||||||
fee_rate: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
class TransactionRepository {
|
class TransactionRepository {
|
||||||
public async $setCluster(txid: string, cluster: string): Promise<void> {
|
public async $setCluster(txid: string, clusterRoot: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await DB.query(
|
await DB.query(
|
||||||
`
|
`
|
||||||
INSERT INTO transactions
|
INSERT INTO compact_transactions
|
||||||
(
|
(
|
||||||
txid,
|
txid,
|
||||||
cluster
|
cluster
|
||||||
)
|
)
|
||||||
VALUE (?, ?)
|
VALUE (UNHEX(?), UNHEX(?))
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
cluster = ?
|
cluster = UNHEX(?)
|
||||||
;`,
|
;`,
|
||||||
[txid, cluster, cluster]
|
[txid, clusterRoot, clusterRoot]
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.err(`Cannot save transaction cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
|
logger.err(`Cannot save transaction cpfp cluster into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
@ -33,19 +25,46 @@ class TransactionRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getCpfpInfo(txid: string): Promise<CpfpInfo | void> {
|
public async $batchSetCluster(txs): Promise<void> {
|
||||||
try {
|
try {
|
||||||
let query = `
|
let query = `
|
||||||
SELECT *
|
INSERT IGNORE INTO compact_transactions
|
||||||
FROM transactions
|
(
|
||||||
LEFT JOIN cpfp_clusters AS cluster ON cluster.root = transactions.cluster
|
txid,
|
||||||
WHERE transactions.txid = ?
|
cluster
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
`;
|
`;
|
||||||
const [rows]: any = await DB.query(query, [txid]);
|
query += txs.map(tx => {
|
||||||
if (rows.length) {
|
return (' (UNHEX(?), UNHEX(?))');
|
||||||
rows[0].txs = JSON.parse(rows[0].txs) as Ancestor[];
|
}) + ';';
|
||||||
if (rows[0]?.txs?.length) {
|
const values = txs.map(tx => [tx.txid, tx.cluster]).flat();
|
||||||
return this.convertCpfp(rows[0]);
|
await DB.query(
|
||||||
|
query,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Cannot save cpfp transactions into db. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $getCpfpInfo(txid: string): Promise<CpfpInfo | void> {
|
||||||
|
try {
|
||||||
|
const [txRows]: any = await DB.query(
|
||||||
|
`
|
||||||
|
SELECT HEX(txid) as id, HEX(cluster) as root
|
||||||
|
FROM compact_transactions
|
||||||
|
WHERE txid = UNHEX(?)
|
||||||
|
`,
|
||||||
|
[txid]
|
||||||
|
);
|
||||||
|
if (txRows.length && txRows[0].root != null) {
|
||||||
|
const txid = txRows[0].id.toLowerCase();
|
||||||
|
const clusterId = txRows[0].root.toLowerCase();
|
||||||
|
const cluster = await cpfpRepository.$getCluster(clusterId);
|
||||||
|
if (cluster) {
|
||||||
|
return this.convertCpfp(txid, cluster);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -54,12 +73,28 @@ class TransactionRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertCpfp(cpfp: CpfpSummary): CpfpInfo {
|
public async $removeTransaction(txid: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await DB.query(
|
||||||
|
`
|
||||||
|
DELETE FROM compact_transactions
|
||||||
|
WHERE txid = UNHEX(?)
|
||||||
|
`,
|
||||||
|
[txid]
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Cannot delete transaction cpfp info from db. Reason: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertCpfp(txid, cluster): CpfpInfo {
|
||||||
const descendants: Ancestor[] = [];
|
const descendants: Ancestor[] = [];
|
||||||
const ancestors: Ancestor[] = [];
|
const ancestors: Ancestor[] = [];
|
||||||
let matched = false;
|
let matched = false;
|
||||||
for (const tx of cpfp.txs) {
|
|
||||||
if (tx.txid === cpfp.txid) {
|
for (const tx of (cluster?.txs || [])) {
|
||||||
|
if (tx.txid === txid) {
|
||||||
matched = true;
|
matched = true;
|
||||||
} else if (!matched) {
|
} else if (!matched) {
|
||||||
descendants.push(tx);
|
descendants.push(tx);
|
||||||
@ -70,7 +105,6 @@ class TransactionRepository {
|
|||||||
return {
|
return {
|
||||||
descendants,
|
descendants,
|
||||||
ancestors,
|
ancestors,
|
||||||
effectiveFeePerVsize: cpfp.fee_rate
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ class NetworkSyncService {
|
|||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
public async $startService(): Promise<void> {
|
public async $startService(): Promise<void> {
|
||||||
logger.info('Starting lightning network sync service');
|
logger.info(`Starting lightning network sync service`, logger.tags.ln);
|
||||||
|
|
||||||
this.loggerTimer = new Date().getTime() / 1000;
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
|
|
||||||
@ -33,11 +33,11 @@ class NetworkSyncService {
|
|||||||
private async $runTasks(): Promise<void> {
|
private async $runTasks(): Promise<void> {
|
||||||
const taskStartTime = Date.now();
|
const taskStartTime = Date.now();
|
||||||
try {
|
try {
|
||||||
logger.info(`Updating nodes and channels`);
|
logger.debug(`Updating nodes and channels`, logger.tags.ln);
|
||||||
|
|
||||||
const networkGraph = await lightningApi.$getNetworkGraph();
|
const networkGraph = await lightningApi.$getNetworkGraph();
|
||||||
if (networkGraph.nodes.length === 0 || networkGraph.edges.length === 0) {
|
if (networkGraph.nodes.length === 0 || networkGraph.edges.length === 0) {
|
||||||
logger.info(`LN Network graph is empty, retrying in 10 seconds`);
|
logger.info(`LN Network graph is empty, retrying in 10 seconds`, logger.tags.ln);
|
||||||
setTimeout(() => { this.$runTasks(); }, 10000);
|
setTimeout(() => { this.$runTasks(); }, 10000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -55,7 +55,7 @@ class NetworkSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$runTasks() error: ' + (e instanceof Error ? e.message : e));
|
logger.err(`$runTasks() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => { this.$runTasks(); }, Math.max(1, (1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL) - (Date.now() - taskStartTime)));
|
setTimeout(() => { this.$runTasks(); }, Math.max(1, (1000 * config.LIGHTNING.GRAPH_REFRESH_INTERVAL) - (Date.now() - taskStartTime)));
|
||||||
@ -79,8 +79,8 @@ class NetworkSyncService {
|
|||||||
++progress;
|
++progress;
|
||||||
|
|
||||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||||
if (elapsedSeconds > 10) {
|
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
|
||||||
logger.info(`Updating node ${progress}/${nodes.length}`);
|
logger.debug(`Updating node ${progress}/${nodes.length}`, logger.tags.ln);
|
||||||
this.loggerTimer = new Date().getTime() / 1000;
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ class NetworkSyncService {
|
|||||||
deletedRecords += await NodeRecordsRepository.$deleteUnusedRecords(node.pub_key, customRecordTypes);
|
deletedRecords += await NodeRecordsRepository.$deleteUnusedRecords(node.pub_key, customRecordTypes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.info(`${progress} nodes updated. ${deletedSockets} sockets deleted. ${deletedRecords} custom records deleted.`);
|
logger.debug(`${progress} nodes updated. ${deletedSockets} sockets deleted. ${deletedRecords} custom records deleted.`);
|
||||||
|
|
||||||
// If a channel if not present in the graph, mark it as inactive
|
// If a channel if not present in the graph, mark it as inactive
|
||||||
await nodesApi.$setNodesInactive(graphNodesPubkeys);
|
await nodesApi.$setNodesInactive(graphNodesPubkeys);
|
||||||
@ -138,18 +138,18 @@ class NetworkSyncService {
|
|||||||
++progress;
|
++progress;
|
||||||
|
|
||||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||||
if (elapsedSeconds > 10) {
|
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
|
||||||
logger.info(`Updating channel ${progress}/${channels.length}`);
|
logger.debug(`Updating channel ${progress}/${channels.length}`, logger.tags.ln);
|
||||||
this.loggerTimer = new Date().getTime() / 1000;
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`${progress} channels updated`);
|
logger.debug(`${progress} channels updated`, logger.tags.ln);
|
||||||
|
|
||||||
// If a channel if not present in the graph, mark it as inactive
|
// If a channel if not present in the graph, mark it as inactive
|
||||||
await channelsApi.$setChannelsInactive(graphChannelsIds);
|
await channelsApi.$setChannelsInactive(graphChannelsIds);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot update channel list. Reason: ${(e instanceof Error ? e.message : e)}`);
|
logger.err(` Cannot update channel list. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.ln);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,44 +184,52 @@ class NetworkSyncService {
|
|||||||
if (lowest < node.first_seen) {
|
if (lowest < node.first_seen) {
|
||||||
const query = `UPDATE nodes SET first_seen = FROM_UNIXTIME(?) WHERE public_key = ?`;
|
const query = `UPDATE nodes SET first_seen = FROM_UNIXTIME(?) WHERE public_key = ?`;
|
||||||
const params = [lowest, node.public_key];
|
const params = [lowest, node.public_key];
|
||||||
|
++updated;
|
||||||
await DB.query(query, params);
|
await DB.query(query, params);
|
||||||
}
|
}
|
||||||
++progress;
|
++progress;
|
||||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||||
if (elapsedSeconds > 10) {
|
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
|
||||||
logger.info(`Updating node first seen date ${progress}/${nodes.length}`);
|
logger.debug(`Updating node first seen date ${progress}/${nodes.length}`, logger.tags.ln);
|
||||||
this.loggerTimer = new Date().getTime() / 1000;
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
++updated;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.info(`Updated ${updated} node first seen dates`);
|
if (updated > 0) {
|
||||||
|
logger.debug(`Updated ${updated} node first seen dates`, logger.tags.ln);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$updateNodeFirstSeen() error: ' + (e instanceof Error ? e.message : e));
|
logger.err(`$updateNodeFirstSeen() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $lookUpCreationDateFromChain(): Promise<void> {
|
private async $lookUpCreationDateFromChain(): Promise<void> {
|
||||||
let progress = 0;
|
let progress = 0;
|
||||||
|
|
||||||
logger.info(`Running channel creation date lookup`);
|
logger.debug(`Running channel creation date lookup`, logger.tags.ln);
|
||||||
try {
|
try {
|
||||||
const channels = await channelsApi.$getChannelsWithoutCreatedDate();
|
const channels = await channelsApi.$getChannelsWithoutCreatedDate();
|
||||||
for (const channel of channels) {
|
for (const channel of channels) {
|
||||||
const transaction = await fundingTxFetcher.$fetchChannelOpenTx(channel.short_id);
|
const transaction = await fundingTxFetcher.$fetchChannelOpenTx(channel.short_id);
|
||||||
|
if (!transaction) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
await DB.query(`
|
await DB.query(`
|
||||||
UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.id = ?`,
|
UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.id = ?`,
|
||||||
[transaction.timestamp, channel.id]
|
[transaction.timestamp, channel.id]
|
||||||
);
|
);
|
||||||
++progress;
|
++progress;
|
||||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||||
if (elapsedSeconds > 10) {
|
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
|
||||||
logger.info(`Updating channel creation date ${progress}/${channels.length}`);
|
logger.debug(`Updating channel creation date ${progress}/${channels.length}`, logger.tags.ln);
|
||||||
this.loggerTimer = new Date().getTime() / 1000;
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.info(`Updated ${channels.length} channels' creation date`);
|
|
||||||
|
if (channels.length > 0) {
|
||||||
|
logger.debug(`Updated ${channels.length} channels' creation date`, logger.tags.ln);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$lookUpCreationDateFromChain() error: ' + (e instanceof Error ? e.message : e));
|
logger.err(`$lookUpCreationDateFromChain() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,7 +238,7 @@ class NetworkSyncService {
|
|||||||
* mark that channel as inactive
|
* mark that channel as inactive
|
||||||
*/
|
*/
|
||||||
private async $deactivateChannelsWithoutActiveNodes(): Promise<void> {
|
private async $deactivateChannelsWithoutActiveNodes(): Promise<void> {
|
||||||
logger.info(`Find channels which nodes are offline`);
|
logger.debug(`Find channels which nodes are offline`, logger.tags.ln);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await DB.query<ResultSetHeader>(`
|
const result = await DB.query<ResultSetHeader>(`
|
||||||
@ -253,12 +261,10 @@ class NetworkSyncService {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
if (result[0].changedRows ?? 0 > 0) {
|
if (result[0].changedRows ?? 0 > 0) {
|
||||||
logger.info(`Marked ${result[0].changedRows} channels as inactive because they are not linked to any active node`);
|
logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not linked to any active node`, logger.tags.ln);
|
||||||
} else {
|
|
||||||
logger.debug(`Marked ${result[0].changedRows} channels as inactive because they are not linked to any active node`);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$deactivateChannelsWithoutActiveNodes() error: ' + (e instanceof Error ? e.message : e));
|
logger.err(`$deactivateChannelsWithoutActiveNodes() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,13 +283,13 @@ class NetworkSyncService {
|
|||||||
} else {
|
} else {
|
||||||
log += ` for the first time`;
|
log += ` for the first time`;
|
||||||
}
|
}
|
||||||
logger.info(log);
|
logger.info(`${log}`, logger.tags.ln);
|
||||||
|
|
||||||
const channels = await channelsApi.$getChannelsByStatus([0, 1]);
|
const channels = await channelsApi.$getChannelsByStatus([0, 1]);
|
||||||
for (const channel of channels) {
|
for (const channel of channels) {
|
||||||
const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout);
|
const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout);
|
||||||
if (spendingTx.spent === true && spendingTx.status?.confirmed === true) {
|
if (spendingTx.spent === true && spendingTx.status?.confirmed === true) {
|
||||||
logger.debug('Marking channel: ' + channel.id + ' as closed.');
|
logger.debug(`Marking channel: ${channel.id} as closed.`, logger.tags.ln);
|
||||||
await DB.query(`UPDATE channels SET status = 2, closing_date = FROM_UNIXTIME(?) WHERE id = ?`,
|
await DB.query(`UPDATE channels SET status = 2, closing_date = FROM_UNIXTIME(?) WHERE id = ?`,
|
||||||
[spendingTx.status.block_time, channel.id]);
|
[spendingTx.status.block_time, channel.id]);
|
||||||
if (spendingTx.txid && !channel.closing_transaction_id) {
|
if (spendingTx.txid && !channel.closing_transaction_id) {
|
||||||
@ -293,16 +299,16 @@ class NetworkSyncService {
|
|||||||
|
|
||||||
++progress;
|
++progress;
|
||||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - this.loggerTimer);
|
||||||
if (elapsedSeconds > 10) {
|
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
|
||||||
logger.info(`Checking if channel has been closed ${progress}/${channels.length}`);
|
logger.info(`Checking if channel has been closed ${progress}/${channels.length}`, logger.tags.ln);
|
||||||
this.loggerTimer = new Date().getTime() / 1000;
|
this.loggerTimer = new Date().getTime() / 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.closedChannelsScanBlock = blocks.getCurrentBlockHeight();
|
this.closedChannelsScanBlock = blocks.getCurrentBlockHeight();
|
||||||
logger.info(`Closed channels scan completed at block ${this.closedChannelsScanBlock}`);
|
logger.debug(`Closed channels scan completed at block ${this.closedChannelsScanBlock}`, logger.tags.ln);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e));
|
logger.err(`$scanForClosedChannels() error: ${e instanceof Error ? e.message : e}`, logger.tags.ln);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import { Common } from '../../api/common';
|
|||||||
|
|
||||||
class LightningStatsUpdater {
|
class LightningStatsUpdater {
|
||||||
public async $startService(): Promise<void> {
|
public async $startService(): Promise<void> {
|
||||||
logger.info('Starting Lightning Stats service');
|
logger.info(`Starting Lightning Stats service`, logger.tags.ln);
|
||||||
|
|
||||||
await this.$runTasks();
|
await this.$runTasks();
|
||||||
LightningStatsImporter.$run();
|
LightningStatsImporter.$run();
|
||||||
@ -27,7 +27,7 @@ class LightningStatsUpdater {
|
|||||||
const networkGraph = await lightningApi.$getNetworkGraph();
|
const networkGraph = await lightningApi.$getNetworkGraph();
|
||||||
await LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph);
|
await LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph);
|
||||||
|
|
||||||
logger.info(`Updated latest network stats`);
|
logger.debug(`Updated latest network stats`, logger.tags.ln);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,10 +21,10 @@ class FundingTxFetcher {
|
|||||||
try {
|
try {
|
||||||
this.fundingTxCache = JSON.parse(await fsPromises.readFile(CACHE_FILE_NAME, 'utf-8'));
|
this.fundingTxCache = JSON.parse(await fsPromises.readFile(CACHE_FILE_NAME, 'utf-8'));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Unable to parse channels funding txs disk cache. Starting from scratch`);
|
logger.err(`Unable to parse channels funding txs disk cache. Starting from scratch`, logger.tags.ln);
|
||||||
this.fundingTxCache = {};
|
this.fundingTxCache = {};
|
||||||
}
|
}
|
||||||
logger.debug(`Imported ${Object.keys(this.fundingTxCache).length} funding tx amount from the disk cache`);
|
logger.debug(`Imported ${Object.keys(this.fundingTxCache).length} funding tx amount from the disk cache`, logger.tags.ln);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,33 +44,34 @@ class FundingTxFetcher {
|
|||||||
++channelProcessed;
|
++channelProcessed;
|
||||||
|
|
||||||
let elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
|
let elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
|
||||||
if (elapsedSeconds > 10) {
|
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
|
||||||
elapsedSeconds = Math.round((new Date().getTime() / 1000) - globalTimer);
|
elapsedSeconds = Math.round((new Date().getTime() / 1000) - globalTimer);
|
||||||
logger.info(`Indexing channels funding tx ${channelProcessed + 1} of ${channelIds.length} ` +
|
logger.info(`Indexing channels funding tx ${channelProcessed + 1} of ${channelIds.length} ` +
|
||||||
`(${Math.floor(channelProcessed / channelIds.length * 10000) / 100}%) | ` +
|
`(${Math.floor(channelProcessed / channelIds.length * 10000) / 100}%) | ` +
|
||||||
`elapsed: ${elapsedSeconds} seconds`
|
`elapsed: ${elapsedSeconds} seconds`,
|
||||||
|
logger.tags.ln
|
||||||
);
|
);
|
||||||
loggerTimer = new Date().getTime() / 1000;
|
loggerTimer = new Date().getTime() / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
elapsedSeconds = Math.round((new Date().getTime() / 1000) - cacheTimer);
|
elapsedSeconds = Math.round((new Date().getTime() / 1000) - cacheTimer);
|
||||||
if (elapsedSeconds > 60) {
|
if (elapsedSeconds > 60) {
|
||||||
logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`);
|
logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`, logger.tags.ln);
|
||||||
fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache));
|
fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache));
|
||||||
cacheTimer = new Date().getTime() / 1000;
|
cacheTimer = new Date().getTime() / 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.channelNewlyProcessed > 0) {
|
if (this.channelNewlyProcessed > 0) {
|
||||||
logger.info(`Indexed ${this.channelNewlyProcessed} additional channels funding tx`);
|
logger.info(`Indexed ${this.channelNewlyProcessed} additional channels funding tx`, logger.tags.ln);
|
||||||
logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`);
|
logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`, logger.tags.ln);
|
||||||
fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache));
|
fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.running = false;
|
this.running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $fetchChannelOpenTx(channelId: string): Promise<{timestamp: number, txid: string, value: number}> {
|
public async $fetchChannelOpenTx(channelId: string): Promise<{timestamp: number, txid: string, value: number} | null> {
|
||||||
channelId = Common.channelIntegerIdToShortId(channelId);
|
channelId = Common.channelIntegerIdToShortId(channelId);
|
||||||
|
|
||||||
if (this.fundingTxCache[channelId]) {
|
if (this.fundingTxCache[channelId]) {
|
||||||
@ -101,6 +102,11 @@ class FundingTxFetcher {
|
|||||||
const rawTx = await bitcoinClient.getRawTransaction(txid);
|
const rawTx = await bitcoinClient.getRawTransaction(txid);
|
||||||
const tx = await bitcoinClient.decodeRawTransaction(rawTx);
|
const tx = await bitcoinClient.decodeRawTransaction(rawTx);
|
||||||
|
|
||||||
|
if (!tx || !tx.vout || tx.vout.length < parseInt(outputIdx, 10) + 1 || tx.vout[outputIdx].value === undefined) {
|
||||||
|
logger.err(`Cannot find blockchain funding tx for channel id ${channelId}. Possible reasons are: bitcoin backend timeout or the channel shortId is not valid`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
this.fundingTxCache[channelId] = {
|
this.fundingTxCache[channelId] = {
|
||||||
timestamp: block.time,
|
timestamp: block.time,
|
||||||
txid: txid,
|
txid: txid,
|
||||||
|
@ -14,7 +14,7 @@ export async function $lookupNodeLocation(): Promise<void> {
|
|||||||
let nodesUpdated = 0;
|
let nodesUpdated = 0;
|
||||||
let geoNamesInserted = 0;
|
let geoNamesInserted = 0;
|
||||||
|
|
||||||
logger.info(`Running node location updater using Maxmind`);
|
logger.debug(`Running node location updater using Maxmind`, logger.tags.ln);
|
||||||
try {
|
try {
|
||||||
const nodes = await nodesApi.$getAllNodes();
|
const nodes = await nodesApi.$getAllNodes();
|
||||||
const lookupCity = await maxmind.open<CityResponse>(config.MAXMIND.GEOLITE2_CITY);
|
const lookupCity = await maxmind.open<CityResponse>(config.MAXMIND.GEOLITE2_CITY);
|
||||||
@ -152,8 +152,8 @@ export async function $lookupNodeLocation(): Promise<void> {
|
|||||||
|
|
||||||
++progress;
|
++progress;
|
||||||
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
|
const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer);
|
||||||
if (elapsedSeconds > 10) {
|
if (elapsedSeconds > config.LIGHTNING.LOGGER_UPDATE_INTERVAL) {
|
||||||
logger.info(`Updating node location data ${progress}/${nodes.length}`);
|
logger.debug(`Updating node location data ${progress}/${nodes.length}`);
|
||||||
loggerTimer = new Date().getTime() / 1000;
|
loggerTimer = new Date().getTime() / 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,9 +161,7 @@ export async function $lookupNodeLocation(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (nodesUpdated > 0) {
|
if (nodesUpdated > 0) {
|
||||||
logger.info(`${nodesUpdated} nodes maxmind data updated, ${geoNamesInserted} geo names inserted`);
|
logger.debug(`${nodesUpdated} nodes maxmind data updated, ${geoNamesInserted} geo names inserted`, logger.tags.ln);
|
||||||
} else {
|
|
||||||
logger.debug(`${nodesUpdated} nodes maxmind data updated, ${geoNamesInserted} geo names inserted`);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('$lookupNodeLocation() error: ' + (e instanceof Error ? e.message : e));
|
logger.err('$lookupNodeLocation() error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
@ -8,7 +8,6 @@ import { isIP } from 'net';
|
|||||||
import { Common } from '../../../api/common';
|
import { Common } from '../../../api/common';
|
||||||
import channelsApi from '../../../api/explorer/channels.api';
|
import channelsApi from '../../../api/explorer/channels.api';
|
||||||
import nodesApi from '../../../api/explorer/nodes.api';
|
import nodesApi from '../../../api/explorer/nodes.api';
|
||||||
import { ResultSetHeader } from 'mysql2';
|
|
||||||
|
|
||||||
const fsPromises = promises;
|
const fsPromises = promises;
|
||||||
|
|
||||||
@ -17,7 +16,7 @@ class LightningStatsImporter {
|
|||||||
|
|
||||||
async $run(): Promise<void> {
|
async $run(): Promise<void> {
|
||||||
const [channels]: any[] = await DB.query('SELECT short_id from channels;');
|
const [channels]: any[] = await DB.query('SELECT short_id from channels;');
|
||||||
logger.info('Caching funding txs for currently existing channels');
|
logger.info(`Caching funding txs for currently existing channels`, logger.tags.ln);
|
||||||
await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id));
|
await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id));
|
||||||
|
|
||||||
if (config.MEMPOOL.NETWORK !== 'mainnet' || config.DATABASE.ENABLED === false) {
|
if (config.MEMPOOL.NETWORK !== 'mainnet' || config.DATABASE.ENABLED === false) {
|
||||||
@ -108,7 +107,7 @@ class LightningStatsImporter {
|
|||||||
|
|
||||||
const tx = await fundingTxFetcher.$fetchChannelOpenTx(short_id);
|
const tx = await fundingTxFetcher.$fetchChannelOpenTx(short_id);
|
||||||
if (!tx) {
|
if (!tx) {
|
||||||
logger.err(`Unable to fetch funding tx for channel ${short_id}. Capacity and creation date is unknown. Skipping channel.`);
|
logger.err(`Unable to fetch funding tx for channel ${short_id}. Capacity and creation date is unknown. Skipping channel.`, logger.tags.ln);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,13 +309,18 @@ class LightningStatsImporter {
|
|||||||
* Import topology files LN historical data into the database
|
* Import topology files LN historical data into the database
|
||||||
*/
|
*/
|
||||||
async $importHistoricalLightningStats(): Promise<void> {
|
async $importHistoricalLightningStats(): Promise<void> {
|
||||||
|
if (!config.LIGHTNING.TOPOLOGY_FOLDER) {
|
||||||
|
logger.info(`Lightning topology folder is not set. Not importing historical LN stats`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug('Run the historical importer');
|
logger.debug('Run the historical importer');
|
||||||
try {
|
try {
|
||||||
let fileList: string[] = [];
|
let fileList: string[] = [];
|
||||||
try {
|
try {
|
||||||
fileList = await fsPromises.readdir(this.topologiesFolder);
|
fileList = await fsPromises.readdir(this.topologiesFolder);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Unable to open topology folder at ${this.topologiesFolder}`);
|
logger.err(`Unable to open topology folder at ${this.topologiesFolder}`, logger.tags.ln);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
// Insert history from the most recent to the oldest
|
// Insert history from the most recent to the oldest
|
||||||
@ -354,7 +358,7 @@ class LightningStatsImporter {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Reading ${this.topologiesFolder}/${filename}`);
|
logger.debug(`Reading ${this.topologiesFolder}/${filename}`, logger.tags.ln);
|
||||||
let fileContent = '';
|
let fileContent = '';
|
||||||
try {
|
try {
|
||||||
fileContent = await fsPromises.readFile(`${this.topologiesFolder}/${filename}`, 'utf8');
|
fileContent = await fsPromises.readFile(`${this.topologiesFolder}/${filename}`, 'utf8');
|
||||||
@ -363,7 +367,7 @@ class LightningStatsImporter {
|
|||||||
totalProcessed++;
|
totalProcessed++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
logger.err(`Unable to open ${this.topologiesFolder}/${filename}`);
|
logger.err(`Unable to open ${this.topologiesFolder}/${filename}`, logger.tags.ln);
|
||||||
totalProcessed++;
|
totalProcessed++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -373,7 +377,7 @@ class LightningStatsImporter {
|
|||||||
graph = JSON.parse(fileContent);
|
graph = JSON.parse(fileContent);
|
||||||
graph = await this.cleanupTopology(graph);
|
graph = await this.cleanupTopology(graph);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content. Reason: ${e instanceof Error ? e.message : e}`);
|
logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln);
|
||||||
totalProcessed++;
|
totalProcessed++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -385,20 +389,20 @@ class LightningStatsImporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!logStarted) {
|
if (!logStarted) {
|
||||||
logger.info(`Founds a topology file that we did not import. Importing historical lightning stats now.`);
|
logger.info(`Founds a topology file that we did not import. Importing historical lightning stats now.`, logger.tags.ln);
|
||||||
logStarted = true;
|
logStarted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`;
|
const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`;
|
||||||
logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.edges.length} channels`);
|
logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.edges.length} channels`, logger.tags.ln);
|
||||||
|
|
||||||
totalProcessed++;
|
totalProcessed++;
|
||||||
|
|
||||||
if (processed > 10) {
|
if (processed > 10) {
|
||||||
logger.info(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`);
|
logger.info(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`, logger.tags.ln);
|
||||||
processed = 0;
|
processed = 0;
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`);
|
logger.debug(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`, logger.tags.ln);
|
||||||
}
|
}
|
||||||
await fundingTxFetcher.$fetchChannelsFundingTxs(graph.edges.map(channel => channel.channel_id.slice(0, -2)));
|
await fundingTxFetcher.$fetchChannelsFundingTxs(graph.edges.map(channel => channel.channel_id.slice(0, -2)));
|
||||||
const stat = await this.computeNetworkStats(timestamp, graph, true);
|
const stat = await this.computeNetworkStats(timestamp, graph, true);
|
||||||
@ -407,10 +411,10 @@ class LightningStatsImporter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (totalProcessed > 0) {
|
if (totalProcessed > 0) {
|
||||||
logger.info(`Lightning network stats historical import completed`);
|
logger.notice(`Lightning network stats historical import completed`, logger.tags.ln);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`);
|
logger.err(`Lightning network stats historical failed. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.ln);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,9 +32,9 @@ class PoolsUpdater {
|
|||||||
this.lastRun = now;
|
this.lastRun = now;
|
||||||
|
|
||||||
if (config.SOCKS5PROXY.ENABLED) {
|
if (config.SOCKS5PROXY.ENABLED) {
|
||||||
logger.info(`Updating latest mining pools from ${this.poolsUrl} over the Tor network`);
|
logger.info(`Updating latest mining pools from ${this.poolsUrl} over the Tor network`, logger.tags.mining);
|
||||||
} else {
|
} else {
|
||||||
logger.info(`Updating latest mining pools from ${this.poolsUrl} over clearnet`);
|
logger.info(`Updating latest mining pools from ${this.poolsUrl} over clearnet`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -53,9 +53,9 @@ class PoolsUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.currentSha === undefined) {
|
if (this.currentSha === undefined) {
|
||||||
logger.info(`Downloading pools.json for the first time from ${this.poolsUrl}`);
|
logger.info(`Downloading pools.json for the first time from ${this.poolsUrl}`, logger.tags.mining);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`Pools.json is outdated, fetch latest from ${this.poolsUrl}`);
|
logger.warn(`Pools.json is outdated, fetch latest from ${this.poolsUrl}`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
const poolsJson = await this.query(this.poolsUrl);
|
const poolsJson = await this.query(this.poolsUrl);
|
||||||
if (poolsJson === undefined) {
|
if (poolsJson === undefined) {
|
||||||
@ -63,11 +63,11 @@ class PoolsUpdater {
|
|||||||
}
|
}
|
||||||
await poolsParser.migratePoolsJson(poolsJson);
|
await poolsParser.migratePoolsJson(poolsJson);
|
||||||
await this.updateDBSha(githubSha);
|
await this.updateDBSha(githubSha);
|
||||||
logger.notice('PoolsUpdater completed');
|
logger.notice(`PoolsUpdater completed`, logger.tags.mining);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
|
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
|
||||||
logger.err('PoolsUpdater failed. Will try again in 24h. Reason: ' + (e instanceof Error ? e.message : e));
|
logger.err(`PoolsUpdater failed. Will try again in 24h. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +81,7 @@ class PoolsUpdater {
|
|||||||
await DB.query('DELETE FROM state where name="pools_json_sha"');
|
await DB.query('DELETE FROM state where name="pools_json_sha"');
|
||||||
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
|
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e));
|
logger.err('Cannot save github pools.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,7 +94,7 @@ class PoolsUpdater {
|
|||||||
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
|
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
|
||||||
return (rows.length > 0 ? rows[0].string : undefined);
|
return (rows.length > 0 ? rows[0].string : undefined);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot fetch pools.json sha from db. Reason: ' + (e instanceof Error ? e.message : e));
|
logger.err('Cannot fetch pools.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,7 +113,7 @@ class PoolsUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.err(`Cannot find "pools.json" in git tree (${this.treeUrl})`);
|
logger.err(`Cannot find "pools.json" in git tree (${this.treeUrl})`, logger.tags.mining);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ class KrakenApi implements PriceFeed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(priceHistory).length > 0) {
|
if (Object.keys(priceHistory).length > 0) {
|
||||||
logger.notice(`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`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
|
import { IConversionRates } from '../mempool.interfaces';
|
||||||
import PricesRepository from '../repositories/PricesRepository';
|
import PricesRepository from '../repositories/PricesRepository';
|
||||||
import BitfinexApi from './price-feeds/bitfinex-api';
|
import BitfinexApi from './price-feeds/bitfinex-api';
|
||||||
import BitflyerApi from './price-feeds/bitflyer-api';
|
import BitflyerApi from './price-feeds/bitflyer-api';
|
||||||
@ -20,17 +21,7 @@ export interface PriceFeed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PriceHistory {
|
export interface PriceHistory {
|
||||||
[timestamp: number]: Prices;
|
[timestamp: number]: IConversionRates;
|
||||||
}
|
|
||||||
|
|
||||||
export interface Prices {
|
|
||||||
USD: number;
|
|
||||||
EUR: number;
|
|
||||||
GBP: number;
|
|
||||||
CAD: number;
|
|
||||||
CHF: number;
|
|
||||||
AUD: number;
|
|
||||||
JPY: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PriceUpdater {
|
class PriceUpdater {
|
||||||
@ -40,7 +31,8 @@ class PriceUpdater {
|
|||||||
running = false;
|
running = false;
|
||||||
feeds: PriceFeed[] = [];
|
feeds: PriceFeed[] = [];
|
||||||
currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
|
currencies: string[] = ['USD', 'EUR', 'GBP', 'CAD', 'CHF', 'AUD', 'JPY'];
|
||||||
latestPrices: Prices;
|
latestPrices: IConversionRates;
|
||||||
|
private ratesChangedCallback: ((rates: IConversionRates) => void) | undefined;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.latestPrices = this.getEmptyPricesObj();
|
this.latestPrices = this.getEmptyPricesObj();
|
||||||
@ -52,18 +44,30 @@ class PriceUpdater {
|
|||||||
this.feeds.push(new GeminiApi());
|
this.feeds.push(new GeminiApi());
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEmptyPricesObj(): Prices {
|
public getEmptyPricesObj(): IConversionRates {
|
||||||
return {
|
return {
|
||||||
USD: -1,
|
USD: 0,
|
||||||
EUR: -1,
|
EUR: 0,
|
||||||
GBP: -1,
|
GBP: 0,
|
||||||
CAD: -1,
|
CAD: 0,
|
||||||
CHF: -1,
|
CHF: 0,
|
||||||
AUD: -1,
|
AUD: 0,
|
||||||
JPY: -1,
|
JPY: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setRatesChangedCallback(fn: (rates: IConversionRates) => void) {
|
||||||
|
this.ratesChangedCallback = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We execute this function before the websocket initialization since
|
||||||
|
* the websocket init is not done asyncronously
|
||||||
|
*/
|
||||||
|
public async $initializeLatestPriceWithDb(): Promise<void> {
|
||||||
|
this.latestPrices = await PricesRepository.$getLatestConversionRates();
|
||||||
|
}
|
||||||
|
|
||||||
public async $run(): Promise<void> {
|
public async $run(): Promise<void> {
|
||||||
if (this.running === true) {
|
if (this.running === true) {
|
||||||
return;
|
return;
|
||||||
@ -76,13 +80,12 @@ class PriceUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
await this.$updatePrice();
|
||||||
if (this.historyInserted === false && config.DATABASE.ENABLED === true) {
|
if (this.historyInserted === false && config.DATABASE.ENABLED === true) {
|
||||||
await this.$insertHistoricalPrices();
|
await this.$insertHistoricalPrices();
|
||||||
} else {
|
|
||||||
await this.$updatePrice();
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`);
|
logger.err(`Cannot save BTC prices in db. Reason: ${e instanceof Error ? e.message : e}`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.running = false;
|
this.running = false;
|
||||||
@ -115,14 +118,14 @@ class PriceUpdater {
|
|||||||
if (price > 0) {
|
if (price > 0) {
|
||||||
prices.push(price);
|
prices.push(price);
|
||||||
}
|
}
|
||||||
logger.debug(`${feed.name} BTC/${currency} price: ${price}`);
|
logger.debug(`${feed.name} BTC/${currency} price: ${price}`, logger.tags.mining);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug(`Could not fetch BTC/${currency} price at ${feed.name}. Reason: ${(e instanceof Error ? e.message : e)}`);
|
logger.debug(`Could not fetch BTC/${currency} price at ${feed.name}. Reason: ${(e instanceof Error ? e.message : e)}`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (prices.length === 1) {
|
if (prices.length === 1) {
|
||||||
logger.debug(`Only ${prices.length} feed available for BTC/${currency} price`);
|
logger.debug(`Only ${prices.length} feed available for BTC/${currency} price`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute average price, non weighted
|
// Compute average price, non weighted
|
||||||
@ -148,6 +151,10 @@ class PriceUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.ratesChangedCallback) {
|
||||||
|
this.ratesChangedCallback(this.latestPrices);
|
||||||
|
}
|
||||||
|
|
||||||
this.lastRun = new Date().getTime() / 1000;
|
this.lastRun = new Date().getTime() / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,9 +186,9 @@ class PriceUpdater {
|
|||||||
++insertedCount;
|
++insertedCount;
|
||||||
}
|
}
|
||||||
if (insertedCount > 0) {
|
if (insertedCount > 0) {
|
||||||
logger.notice(`Inserted ${insertedCount} MtGox USD weekly price history into db`);
|
logger.notice(`Inserted ${insertedCount} MtGox USD weekly price history into db`, logger.tags.mining);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Inserted ${insertedCount} MtGox USD weekly price history into db`);
|
logger.debug(`Inserted ${insertedCount} MtGox USD weekly price history into db`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert Kraken weekly prices
|
// Insert Kraken weekly prices
|
||||||
@ -202,7 +209,7 @@ class PriceUpdater {
|
|||||||
private async $insertMissingRecentPrices(type: 'hour' | 'day'): Promise<void> {
|
private async $insertMissingRecentPrices(type: 'hour' | 'day'): Promise<void> {
|
||||||
const existingPriceTimes = await PricesRepository.$getPricesTimes();
|
const existingPriceTimes = await PricesRepository.$getPricesTimes();
|
||||||
|
|
||||||
logger.info(`Fetching ${type === 'day' ? 'dai' : 'hour'}ly price history from exchanges and saving missing ones into the database, this may take a while`);
|
logger.info(`Fetching ${type === 'day' ? 'dai' : 'hour'}ly price history from exchanges and saving missing ones into the database`, logger.tags.mining);
|
||||||
|
|
||||||
const historicalPrices: PriceHistory[] = [];
|
const historicalPrices: PriceHistory[] = [];
|
||||||
|
|
||||||
@ -211,13 +218,13 @@ class PriceUpdater {
|
|||||||
try {
|
try {
|
||||||
historicalPrices.push(await feed.$fetchRecentPrice(this.currencies, type));
|
historicalPrices.push(await feed.$fetchRecentPrice(this.currencies, type));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`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}`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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];
|
||||||
const grouped: Object = {};
|
const grouped: any = {};
|
||||||
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))) {
|
||||||
@ -233,7 +240,7 @@ class PriceUpdater {
|
|||||||
for (const currency of this.currencies) {
|
for (const currency of this.currencies) {
|
||||||
const price = historicalEntry[time][currency];
|
const price = historicalEntry[time][currency];
|
||||||
if (price > 0) {
|
if (price > 0) {
|
||||||
grouped[time][currency].push(parseInt(price, 10));
|
grouped[time][currency].push(typeof price === 'string' ? parseInt(price, 10) : price);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -242,7 +249,7 @@ class PriceUpdater {
|
|||||||
// Average prices and insert everything into the db
|
// Average prices and insert everything into the db
|
||||||
let totalInserted = 0;
|
let totalInserted = 0;
|
||||||
for (const time in grouped) {
|
for (const time in grouped) {
|
||||||
const prices: Prices = this.getEmptyPricesObj();
|
const prices: IConversionRates = this.getEmptyPricesObj();
|
||||||
for (const currency in grouped[time]) {
|
for (const currency in grouped[time]) {
|
||||||
if (grouped[time][currency].length === 0) {
|
if (grouped[time][currency].length === 0) {
|
||||||
continue;
|
continue;
|
||||||
@ -256,9 +263,9 @@ class PriceUpdater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (totalInserted > 0) {
|
if (totalInserted > 0) {
|
||||||
logger.notice(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`);
|
logger.notice(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`, logger.tags.mining);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`);
|
logger.debug(`Inserted ${totalInserted} ${type === 'day' ? 'dai' : 'hour'}ly historical prices into the db`, logger.tags.mining);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export function prepareBlock(block: any): BlockExtended {
|
|||||||
extras: {
|
extras: {
|
||||||
coinbaseRaw: block.coinbase_raw ?? block.extras?.coinbaseRaw,
|
coinbaseRaw: block.coinbase_raw ?? block.extras?.coinbaseRaw,
|
||||||
medianFee: block.medianFee ?? block.median_fee ?? block.extras?.medianFee,
|
medianFee: block.medianFee ?? block.median_fee ?? block.extras?.medianFee,
|
||||||
feeRange: block.feeRange ?? block.fee_span,
|
feeRange: block.feeRange ?? block?.extras?.feeRange ?? block.fee_span,
|
||||||
reward: block.reward ?? block?.extras?.reward,
|
reward: block.reward ?? block?.extras?.reward,
|
||||||
totalFees: block.totalFees ?? block?.fees ?? block?.extras?.totalFees,
|
totalFees: block.totalFees ?? block?.fees ?? block?.extras?.totalFees,
|
||||||
avgFee: block?.extras?.avgFee ?? block.avg_fee,
|
avgFee: block?.extras?.avgFee ?? block.avg_fee,
|
||||||
|
14
backend/src/utils/clone.ts
Normal file
14
backend/src/utils/clone.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// simple recursive deep clone for literal-type objects
|
||||||
|
// does not preserve Dates, Maps, Sets etc
|
||||||
|
// does not support recursive objects
|
||||||
|
// properties deeper than maxDepth will be shallow cloned
|
||||||
|
export function deepClone(obj: any, maxDepth: number = 50, depth: number = 0): any {
|
||||||
|
let cloned = obj;
|
||||||
|
if (depth < maxDepth && typeof obj === 'object') {
|
||||||
|
cloned = Array.isArray(obj) ? [] : {};
|
||||||
|
for (const key in obj) {
|
||||||
|
cloned[key] = deepClone(obj[key], maxDepth, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cloned;
|
||||||
|
}
|
3
contributors/AlexLloyd0.txt
Normal file
3
contributors/AlexLloyd0.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
|
||||||
|
|
||||||
|
Signed: AlexLloyd0
|
3
contributors/Arooba-git.txt
Normal file
3
contributors/Arooba-git.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of January 25, 2022.
|
||||||
|
|
||||||
|
Signed: Arooba-git
|
3
contributors/piterden.txt
Normal file
3
contributors/piterden.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of December 17, 2022.
|
||||||
|
|
||||||
|
Signed: piterden
|
@ -17,7 +17,7 @@ _Note: address lookups require an Electrum Server and will not work with this co
|
|||||||
|
|
||||||
The default Docker configuration assumes you have the following configuration in your `bitcoin.conf` file:
|
The default Docker configuration assumes you have the following configuration in your `bitcoin.conf` file:
|
||||||
|
|
||||||
```
|
```ini
|
||||||
txindex=1
|
txindex=1
|
||||||
server=1
|
server=1
|
||||||
rpcuser=mempool
|
rpcuser=mempool
|
||||||
@ -26,7 +26,7 @@ rpcpassword=mempool
|
|||||||
|
|
||||||
If you want to use different credentials, specify them in the `docker-compose.yml` file:
|
If you want to use different credentials, specify them in the `docker-compose.yml` file:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
MEMPOOL_BACKEND: "none"
|
MEMPOOL_BACKEND: "none"
|
||||||
@ -54,7 +54,7 @@ First, configure `bitcoind` as specified above, and make sure your Electrum Serv
|
|||||||
|
|
||||||
Then, set the following variables in `docker-compose.yml` so Mempool can connect to your Electrum Server:
|
Then, set the following variables in `docker-compose.yml` so Mempool can connect to your Electrum Server:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
MEMPOOL_BACKEND: "electrum"
|
MEMPOOL_BACKEND: "electrum"
|
||||||
@ -85,7 +85,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
`mempool-config.json`:
|
`mempool-config.json`:
|
||||||
```
|
```json
|
||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
"NETWORK": "mainnet",
|
"NETWORK": "mainnet",
|
||||||
"BACKEND": "electrum",
|
"BACKEND": "electrum",
|
||||||
@ -100,17 +100,23 @@ Below we list all settings from `mempool-config.json` and the corresponding over
|
|||||||
"BLOCK_WEIGHT_UNITS": 4000000,
|
"BLOCK_WEIGHT_UNITS": 4000000,
|
||||||
"INITIAL_BLOCKS_AMOUNT": 8,
|
"INITIAL_BLOCKS_AMOUNT": 8,
|
||||||
"MEMPOOL_BLOCKS_AMOUNT": 8,
|
"MEMPOOL_BLOCKS_AMOUNT": 8,
|
||||||
|
"BLOCKS_SUMMARIES_INDEXING": false,
|
||||||
"PRICE_FEED_UPDATE_INTERVAL": 600,
|
"PRICE_FEED_UPDATE_INTERVAL": 600,
|
||||||
"USE_SECOND_NODE_FOR_MINFEE": false,
|
"USE_SECOND_NODE_FOR_MINFEE": false,
|
||||||
"EXTERNAL_ASSETS": ["https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json"],
|
"EXTERNAL_ASSETS": ["https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json"],
|
||||||
"STDOUT_LOG_MIN_PRIORITY": "info",
|
"STDOUT_LOG_MIN_PRIORITY": "info",
|
||||||
|
"INDEXING_BLOCKS_AMOUNT": false,
|
||||||
|
"AUTOMATIC_BLOCK_REINDEXING": false,
|
||||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
|
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json",
|
||||||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master"
|
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||||
|
"ADVANCED_GBT_AUDIT": false,
|
||||||
|
"ADVANCED_GBT_MEMPOOL": false,
|
||||||
|
"CPFP_INDEXING": false,
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
Corresponding `docker-compose.yml` overrides:
|
Corresponding `docker-compose.yml` overrides:
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
MEMPOOL_NETWORK: ""
|
MEMPOOL_NETWORK: ""
|
||||||
@ -125,20 +131,30 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
MEMPOOL_BLOCK_WEIGHT_UNITS: ""
|
MEMPOOL_BLOCK_WEIGHT_UNITS: ""
|
||||||
MEMPOOL_INITIAL_BLOCKS_AMOUNT: ""
|
MEMPOOL_INITIAL_BLOCKS_AMOUNT: ""
|
||||||
MEMPOOL_MEMPOOL_BLOCKS_AMOUNT: ""
|
MEMPOOL_MEMPOOL_BLOCKS_AMOUNT: ""
|
||||||
|
MEMPOOL_BLOCKS_SUMMARIES_INDEXING: ""
|
||||||
MEMPOOL_PRICE_FEED_UPDATE_INTERVAL: ""
|
MEMPOOL_PRICE_FEED_UPDATE_INTERVAL: ""
|
||||||
MEMPOOL_USE_SECOND_NODE_FOR_MINFEE: ""
|
MEMPOOL_USE_SECOND_NODE_FOR_MINFEE: ""
|
||||||
MEMPOOL_EXTERNAL_ASSETS: ""
|
MEMPOOL_EXTERNAL_ASSETS: ""
|
||||||
MEMPOOL_STDOUT_LOG_MIN_PRIORITY: ""
|
MEMPOOL_STDOUT_LOG_MIN_PRIORITY: ""
|
||||||
|
MEMPOOL_INDEXING_BLOCKS_AMOUNT: ""
|
||||||
|
MEMPOOL_AUTOMATIC_BLOCK_REINDEXING: ""
|
||||||
MEMPOOL_POOLS_JSON_URL: ""
|
MEMPOOL_POOLS_JSON_URL: ""
|
||||||
MEMPOOL_POOLS_JSON_TREE_URL: ""
|
MEMPOOL_POOLS_JSON_TREE_URL: ""
|
||||||
|
MEMPOOL_ADVANCED_GBT_AUDIT: ""
|
||||||
|
MEMPOOL_ADVANCED_GBT_MEMPOOL: ""
|
||||||
|
MEMPOOL_CPFP_INDEXING: ""
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`ADVANCED_GBT_AUDIT` AND `ADVANCED_GBT_MEMPOOL` enable a more accurate (but slower) block prediction algorithm for the block audit feature and the projected mempool-blocks respectively.
|
||||||
|
|
||||||
|
`CPFP_INDEXING` enables indexing CPFP (Child Pays For Parent) information for the last `INDEXING_BLOCKS_AMOUNT` blocks.
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
`mempool-config.json`:
|
`mempool-config.json`:
|
||||||
```
|
```json
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
"PORT": 8332,
|
"PORT": 8332,
|
||||||
"USERNAME": "mempool",
|
"USERNAME": "mempool",
|
||||||
@ -147,7 +163,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Corresponding `docker-compose.yml` overrides:
|
Corresponding `docker-compose.yml` overrides:
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
CORE_RPC_HOST: ""
|
CORE_RPC_HOST: ""
|
||||||
@ -160,7 +176,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
`mempool-config.json`:
|
`mempool-config.json`:
|
||||||
```
|
```json
|
||||||
"ELECTRUM": {
|
"ELECTRUM": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
"PORT": 50002,
|
"PORT": 50002,
|
||||||
@ -169,7 +185,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Corresponding `docker-compose.yml` overrides:
|
Corresponding `docker-compose.yml` overrides:
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
ELECTRUM_HOST: ""
|
ELECTRUM_HOST: ""
|
||||||
@ -181,14 +197,14 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
`mempool-config.json`:
|
`mempool-config.json`:
|
||||||
```
|
```json
|
||||||
"ESPLORA": {
|
"ESPLORA": {
|
||||||
"REST_API_URL": "http://127.0.0.1:3000"
|
"REST_API_URL": "http://127.0.0.1:3000"
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
Corresponding `docker-compose.yml` overrides:
|
Corresponding `docker-compose.yml` overrides:
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
ESPLORA_REST_API_URL: ""
|
ESPLORA_REST_API_URL: ""
|
||||||
@ -198,7 +214,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
`mempool-config.json`:
|
`mempool-config.json`:
|
||||||
```
|
```json
|
||||||
"SECOND_CORE_RPC": {
|
"SECOND_CORE_RPC": {
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
"PORT": 8332,
|
"PORT": 8332,
|
||||||
@ -208,7 +224,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Corresponding `docker-compose.yml` overrides:
|
Corresponding `docker-compose.yml` overrides:
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
SECOND_CORE_RPC_HOST: ""
|
SECOND_CORE_RPC_HOST: ""
|
||||||
@ -221,7 +237,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
`mempool-config.json`:
|
`mempool-config.json`:
|
||||||
```
|
```json
|
||||||
"DATABASE": {
|
"DATABASE": {
|
||||||
"ENABLED": true,
|
"ENABLED": true,
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
@ -233,7 +249,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Corresponding `docker-compose.yml` overrides:
|
Corresponding `docker-compose.yml` overrides:
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
DATABASE_ENABLED: ""
|
DATABASE_ENABLED: ""
|
||||||
@ -248,7 +264,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
`mempool-config.json`:
|
`mempool-config.json`:
|
||||||
```
|
```json
|
||||||
"SYSLOG": {
|
"SYSLOG": {
|
||||||
"ENABLED": true,
|
"ENABLED": true,
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
@ -259,7 +275,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Corresponding `docker-compose.yml` overrides:
|
Corresponding `docker-compose.yml` overrides:
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
SYSLOG_ENABLED: ""
|
SYSLOG_ENABLED: ""
|
||||||
@ -273,7 +289,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
`mempool-config.json`:
|
`mempool-config.json`:
|
||||||
```
|
```json
|
||||||
"STATISTICS": {
|
"STATISTICS": {
|
||||||
"ENABLED": true,
|
"ENABLED": true,
|
||||||
"TX_PER_SECOND_SAMPLE_PERIOD": 150
|
"TX_PER_SECOND_SAMPLE_PERIOD": 150
|
||||||
@ -281,7 +297,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Corresponding `docker-compose.yml` overrides:
|
Corresponding `docker-compose.yml` overrides:
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
STATISTICS_ENABLED: ""
|
STATISTICS_ENABLED: ""
|
||||||
@ -292,7 +308,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
`mempool-config.json`:
|
`mempool-config.json`:
|
||||||
```
|
```json
|
||||||
"BISQ": {
|
"BISQ": {
|
||||||
"ENABLED": false,
|
"ENABLED": false,
|
||||||
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
|
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
|
||||||
@ -300,7 +316,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Corresponding `docker-compose.yml` overrides:
|
Corresponding `docker-compose.yml` overrides:
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
BISQ_ENABLED: ""
|
BISQ_ENABLED: ""
|
||||||
@ -311,7 +327,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
`mempool-config.json`:
|
`mempool-config.json`:
|
||||||
```
|
```json
|
||||||
"SOCKS5PROXY": {
|
"SOCKS5PROXY": {
|
||||||
"ENABLED": false,
|
"ENABLED": false,
|
||||||
"HOST": "127.0.0.1",
|
"HOST": "127.0.0.1",
|
||||||
@ -322,7 +338,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Corresponding `docker-compose.yml` overrides:
|
Corresponding `docker-compose.yml` overrides:
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
SOCKS5PROXY_ENABLED: ""
|
SOCKS5PROXY_ENABLED: ""
|
||||||
@ -336,7 +352,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
`mempool-config.json`:
|
`mempool-config.json`:
|
||||||
```
|
```json
|
||||||
"PRICE_DATA_SERVER": {
|
"PRICE_DATA_SERVER": {
|
||||||
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
|
"TOR_URL": "http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices",
|
||||||
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
|
"CLEARNET_URL": "https://price.bisq.wiz.biz/getAllMarketPrices"
|
||||||
@ -344,7 +360,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Corresponding `docker-compose.yml` overrides:
|
Corresponding `docker-compose.yml` overrides:
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
PRICE_DATA_SERVER_TOR_URL: ""
|
PRICE_DATA_SERVER_TOR_URL: ""
|
||||||
@ -355,7 +371,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
`mempool-config.json`:
|
`mempool-config.json`:
|
||||||
```
|
```json
|
||||||
"LIGHTNING": {
|
"LIGHTNING": {
|
||||||
"ENABLED": false
|
"ENABLED": false
|
||||||
"BACKEND": "lnd"
|
"BACKEND": "lnd"
|
||||||
@ -367,7 +383,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Corresponding `docker-compose.yml` overrides:
|
Corresponding `docker-compose.yml` overrides:
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
LIGHTNING_ENABLED: false
|
LIGHTNING_ENABLED: false
|
||||||
@ -382,7 +398,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
`mempool-config.json`:
|
`mempool-config.json`:
|
||||||
```
|
```json
|
||||||
"LND": {
|
"LND": {
|
||||||
"TLS_CERT_PATH": ""
|
"TLS_CERT_PATH": ""
|
||||||
"MACAROON_PATH": ""
|
"MACAROON_PATH": ""
|
||||||
@ -391,7 +407,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Corresponding `docker-compose.yml` overrides:
|
Corresponding `docker-compose.yml` overrides:
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
LND_TLS_CERT_PATH: ""
|
LND_TLS_CERT_PATH: ""
|
||||||
@ -403,14 +419,14 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
`mempool-config.json`:
|
`mempool-config.json`:
|
||||||
```
|
```json
|
||||||
"CLIGHTNING": {
|
"CLIGHTNING": {
|
||||||
"SOCKET": ""
|
"SOCKET": ""
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Corresponding `docker-compose.yml` overrides:
|
Corresponding `docker-compose.yml` overrides:
|
||||||
```
|
```yaml
|
||||||
api:
|
api:
|
||||||
environment:
|
environment:
|
||||||
CLIGHTNING_SOCKET: ""
|
CLIGHTNING_SOCKET: ""
|
||||||
|
@ -22,7 +22,11 @@
|
|||||||
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
|
"STDOUT_LOG_MIN_PRIORITY": "__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__",
|
||||||
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__,
|
"INDEXING_BLOCKS_AMOUNT": __MEMPOOL_INDEXING_BLOCKS_AMOUNT__,
|
||||||
"BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__,
|
"BLOCKS_SUMMARIES_INDEXING": __MEMPOOL_BLOCKS_SUMMARIES_INDEXING__,
|
||||||
"AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__
|
"AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__,
|
||||||
|
"AUDIT": __MEMPOOL_AUDIT__,
|
||||||
|
"ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__,
|
||||||
|
"ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__,
|
||||||
|
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__
|
||||||
},
|
},
|
||||||
"CORE_RPC": {
|
"CORE_RPC": {
|
||||||
"HOST": "__CORE_RPC_HOST__",
|
"HOST": "__CORE_RPC_HOST__",
|
||||||
|
@ -27,6 +27,10 @@ __MEMPOOL_INDEXING_BLOCKS_AMOUNT__=${MEMPOOL_INDEXING_BLOCKS_AMOUNT:=false}
|
|||||||
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
|
__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__=${MEMPOOL_AUTOMATIC_BLOCK_REINDEXING:=false}
|
||||||
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json}
|
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools.json}
|
||||||
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
|
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
|
||||||
|
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
|
||||||
|
__MEMPOOL_ADVANCED_GBT_AUDIT__=${MEMPOOL_ADVANCED_GBT_AUDIT:=false}
|
||||||
|
__MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false}
|
||||||
|
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
||||||
|
|
||||||
# CORE_RPC
|
# CORE_RPC
|
||||||
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
|
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
|
||||||
@ -136,6 +140,10 @@ sed -i "s/__MEMPOOL_INDEXING_BLOCKS_AMOUNT__/${__MEMPOOL_INDEXING_BLOCKS_AMOUNT_
|
|||||||
sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json
|
sed -i "s/__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__/${__MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__}/g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
|
||||||
|
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
|
||||||
|
sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g" mempool-config.json
|
||||||
|
sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json
|
||||||
|
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
|
||||||
|
|
||||||
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
|
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
|
||||||
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json
|
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json
|
||||||
|
@ -31,6 +31,10 @@ __LIQUID_WEBSITE_URL__=${LIQUID_WEBSITE_URL:=https://liquid.network}
|
|||||||
__BISQ_WEBSITE_URL__=${BISQ_WEBSITE_URL:=https://bisq.markets}
|
__BISQ_WEBSITE_URL__=${BISQ_WEBSITE_URL:=https://bisq.markets}
|
||||||
__MINING_DASHBOARD__=${MINING_DASHBOARD:=true}
|
__MINING_DASHBOARD__=${MINING_DASHBOARD:=true}
|
||||||
__LIGHTNING__=${LIGHTNING:=false}
|
__LIGHTNING__=${LIGHTNING:=false}
|
||||||
|
__AUDIT__=${AUDIT:=false}
|
||||||
|
__MAINNET_BLOCK_AUDIT_START_HEIGHT__=${MAINNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||||
|
__TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||||
|
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||||
|
|
||||||
# Export as environment variables to be used by envsubst
|
# Export as environment variables to be used by envsubst
|
||||||
export __TESTNET_ENABLED__
|
export __TESTNET_ENABLED__
|
||||||
@ -52,6 +56,10 @@ export __LIQUID_WEBSITE_URL__
|
|||||||
export __BISQ_WEBSITE_URL__
|
export __BISQ_WEBSITE_URL__
|
||||||
export __MINING_DASHBOARD__
|
export __MINING_DASHBOARD__
|
||||||
export __LIGHTNING__
|
export __LIGHTNING__
|
||||||
|
export __AUDIT__
|
||||||
|
export __MAINNET_BLOCK_AUDIT_START_HEIGHT__
|
||||||
|
export __TESTNET_BLOCK_AUDIT_START_HEIGHT__
|
||||||
|
export __SIGNET_BLOCK_AUDIT_START_HEIGHT__
|
||||||
|
|
||||||
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
|
folder=$(find /var/www/mempool -name "config.js" | xargs dirname)
|
||||||
echo ${folder}
|
echo ${folder}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
[main]
|
[main]
|
||||||
host = https://www.transifex.com
|
host = https://www.transifex.com
|
||||||
|
|
||||||
[mempool.frontend-src-locale-messages-xlf--master]
|
[o:mempool:p:mempool:r:frontend-src-locale-messages-xlf--master]
|
||||||
file_filter = frontend/src/locale/messages.<lang>.xlf
|
file_filter = frontend/src/locale/messages.<lang>.xlf
|
||||||
|
source_file = frontend/src/locale/messages.en-US.xlf
|
||||||
source_lang = en-US
|
source_lang = en-US
|
||||||
type = XLIFF
|
type = XLIFF
|
||||||
|
|
||||||
|
@ -127,8 +127,9 @@ https://www.transifex.com/mempool/mempool/dashboard/
|
|||||||
* Thai @Gusb3ll
|
* Thai @Gusb3ll
|
||||||
* Turkish @stackmore
|
* Turkish @stackmore
|
||||||
* Ukrainian @volbil
|
* Ukrainian @volbil
|
||||||
* Vietnamese @bitcoin_vietnam
|
* Vietnamese @BitcoinvnNews
|
||||||
* Chinese @wdljt
|
* Chinese @wdljt
|
||||||
* Russian @TonyCrusoe @Bitconan
|
* Russian @TonyCrusoe @Bitconan
|
||||||
* Romanian @mirceavesa
|
* Romanian @mirceavesa
|
||||||
* Macedonian @SkechBoy
|
* Macedonian @SkechBoy
|
||||||
|
* Nepalese @kebinm
|
||||||
|
@ -137,6 +137,14 @@
|
|||||||
"hi": {
|
"hi": {
|
||||||
"translation": "src/locale/messages.hi.xlf",
|
"translation": "src/locale/messages.hi.xlf",
|
||||||
"baseHref": "/hi/"
|
"baseHref": "/hi/"
|
||||||
|
},
|
||||||
|
"ne": {
|
||||||
|
"translation": "src/locale/messages.ne.xlf",
|
||||||
|
"baseHref": "/ne/"
|
||||||
|
},
|
||||||
|
"lt": {
|
||||||
|
"translation": "src/locale/messages.lt.xlf",
|
||||||
|
"baseHref": "/lt/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -7,7 +7,6 @@ describe('Liquid', () => {
|
|||||||
cy.intercept('/liquid/api/blocks/').as('blocks');
|
cy.intercept('/liquid/api/blocks/').as('blocks');
|
||||||
cy.intercept('/liquid/api/tx/**/outspends').as('outspends');
|
cy.intercept('/liquid/api/tx/**/outspends').as('outspends');
|
||||||
cy.intercept('/liquid/api/block/**/txs/**').as('block-txs');
|
cy.intercept('/liquid/api/block/**/txs/**').as('block-txs');
|
||||||
cy.intercept('/resources/pools.json').as('pools');
|
|
||||||
|
|
||||||
Cypress.Commands.add('waitForBlockData', () => {
|
Cypress.Commands.add('waitForBlockData', () => {
|
||||||
cy.wait('@socket');
|
cy.wait('@socket');
|
||||||
|
@ -7,7 +7,6 @@ describe('Liquid Testnet', () => {
|
|||||||
cy.intercept('/liquidtestnet/api/blocks/').as('blocks');
|
cy.intercept('/liquidtestnet/api/blocks/').as('blocks');
|
||||||
cy.intercept('/liquidtestnet/api/tx/**/outspends').as('outspends');
|
cy.intercept('/liquidtestnet/api/tx/**/outspends').as('outspends');
|
||||||
cy.intercept('/liquidtestnet/api/block/**/txs/**').as('block-txs');
|
cy.intercept('/liquidtestnet/api/block/**/txs/**').as('block-txs');
|
||||||
cy.intercept('/resources/pools.json').as('pools');
|
|
||||||
|
|
||||||
Cypress.Commands.add('waitForBlockData', () => {
|
Cypress.Commands.add('waitForBlockData', () => {
|
||||||
cy.wait('@socket');
|
cy.wait('@socket');
|
||||||
|
@ -41,7 +41,6 @@ describe('Mainnet', () => {
|
|||||||
// cy.intercept('/api/v1/block/*/summary').as('block-summary');
|
// cy.intercept('/api/v1/block/*/summary').as('block-summary');
|
||||||
// cy.intercept('/api/v1/outspends/*').as('outspends');
|
// cy.intercept('/api/v1/outspends/*').as('outspends');
|
||||||
// cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
// cy.intercept('/api/tx/*/outspends').as('tx-outspends');
|
||||||
// cy.intercept('/resources/pools.json').as('pools');
|
|
||||||
|
|
||||||
// Search Auto Complete
|
// Search Auto Complete
|
||||||
cy.intercept('/api/address-prefix/1wiz').as('search-1wiz');
|
cy.intercept('/api/address-prefix/1wiz').as('search-1wiz');
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
"LIQUID_WEBSITE_URL": "https://liquid.network",
|
"LIQUID_WEBSITE_URL": "https://liquid.network",
|
||||||
"BISQ_WEBSITE_URL": "https://bisq.markets",
|
"BISQ_WEBSITE_URL": "https://bisq.markets",
|
||||||
"MINING_DASHBOARD": true,
|
"MINING_DASHBOARD": true,
|
||||||
|
"AUDIT": false,
|
||||||
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
||||||
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
||||||
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
||||||
|
357
frontend/package-lock.json
generated
357
frontend/package-lock.json
generated
@ -31,15 +31,14 @@
|
|||||||
"bootstrap": "~4.6.1",
|
"bootstrap": "~4.6.1",
|
||||||
"browserify": "^17.0.0",
|
"browserify": "^17.0.0",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"cypress": "^12.1.0",
|
|
||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
"echarts": "~5.4.0",
|
"echarts": "~5.4.1",
|
||||||
"echarts-gl": "^2.0.9",
|
"echarts-gl": "^2.0.9",
|
||||||
"lightweight-charts": "~3.8.0",
|
"lightweight-charts": "~3.8.0",
|
||||||
"ngx-echarts": "~14.0.0",
|
"ngx-echarts": "~14.0.0",
|
||||||
"ngx-infinite-scroll": "^14.0.1",
|
"ngx-infinite-scroll": "^14.0.1",
|
||||||
"qrcode": "1.5.1",
|
"qrcode": "1.5.1",
|
||||||
"rxjs": "~7.5.7",
|
"rxjs": "~7.8.0",
|
||||||
"tinyify": "^3.1.0",
|
"tinyify": "^3.1.0",
|
||||||
"tlite": "^0.1.9",
|
"tlite": "^0.1.9",
|
||||||
"tslib": "~2.4.1",
|
"tslib": "~2.4.1",
|
||||||
@ -49,17 +48,17 @@
|
|||||||
"@angular/compiler-cli": "^14.2.12",
|
"@angular/compiler-cli": "^14.2.12",
|
||||||
"@angular/language-service": "^14.2.12",
|
"@angular/language-service": "^14.2.12",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.48.1",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.31.0",
|
||||||
"http-proxy-middleware": "~2.0.6",
|
"http-proxy-middleware": "~2.0.6",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.2",
|
||||||
"ts-node": "~10.9.1",
|
"ts-node": "~10.9.1",
|
||||||
"typescript": "~4.6.4"
|
"typescript": "~4.6.4"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@cypress/schematic": "^2.4.0",
|
"@cypress/schematic": "^2.4.0",
|
||||||
"cypress": "^12.1.0",
|
"cypress": "^12.3.0",
|
||||||
"cypress-fail-on-console-error": "~4.0.2",
|
"cypress-fail-on-console-error": "~4.0.2",
|
||||||
"cypress-wait-until": "^1.7.2",
|
"cypress-wait-until": "^1.7.2",
|
||||||
"mock-socket": "~9.1.5",
|
"mock-socket": "~9.1.5",
|
||||||
@ -3203,15 +3202,6 @@
|
|||||||
"verror": "1.10.0"
|
"verror": "1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cypress/request/node_modules/qs": {
|
|
||||||
"version": "6.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
|
||||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@cypress/request/node_modules/tough-cookie": {
|
"node_modules/@cypress/request/node_modules/tough-cookie": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||||
@ -3382,15 +3372,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc": {
|
"node_modules/@eslint/eslintrc": {
|
||||||
"version": "1.3.3",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz",
|
||||||
"integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==",
|
"integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^6.12.4",
|
"ajv": "^6.12.4",
|
||||||
"debug": "^4.3.2",
|
"debug": "^4.3.2",
|
||||||
"espree": "^9.4.0",
|
"espree": "^9.4.0",
|
||||||
"globals": "^13.15.0",
|
"globals": "^13.19.0",
|
||||||
"ignore": "^5.2.0",
|
"ignore": "^5.2.0",
|
||||||
"import-fresh": "^3.2.1",
|
"import-fresh": "^3.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
@ -3411,9 +3401,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
||||||
"version": "13.18.0",
|
"version": "13.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz",
|
||||||
"integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==",
|
"integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"type-fest": "^0.20.2"
|
"type-fest": "^0.20.2"
|
||||||
@ -3562,9 +3552,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.7",
|
"version": "0.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
|
||||||
"integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==",
|
"integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@humanwhocodes/object-schema": "^1.2.1",
|
"@humanwhocodes/object-schema": "^1.2.1",
|
||||||
@ -4294,14 +4284,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.1.tgz",
|
||||||
"integrity": "sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA==",
|
"integrity": "sha512-9nY5K1Rp2ppmpb9s9S2aBiF3xo5uExCehMDmYmmFqqyxgenbHJ3qbarcLt4ITgaD6r/2ypdlcFRdcuVPnks+fQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "5.45.0",
|
"@typescript-eslint/scope-manager": "5.48.1",
|
||||||
"@typescript-eslint/type-utils": "5.45.0",
|
"@typescript-eslint/type-utils": "5.48.1",
|
||||||
"@typescript-eslint/utils": "5.45.0",
|
"@typescript-eslint/utils": "5.48.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ignore": "^5.2.0",
|
"ignore": "^5.2.0",
|
||||||
"natural-compare-lite": "^1.4.0",
|
"natural-compare-lite": "^1.4.0",
|
||||||
@ -4344,14 +4334,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.48.1.tgz",
|
||||||
"integrity": "sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ==",
|
"integrity": "sha512-4yg+FJR/V1M9Xoq56SF9Iygqm+r5LMXvheo6DQ7/yUWynQ4YfCRnsKuRgqH4EQ5Ya76rVwlEpw4Xu+TgWQUcdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "5.45.0",
|
"@typescript-eslint/scope-manager": "5.48.1",
|
||||||
"@typescript-eslint/types": "5.45.0",
|
"@typescript-eslint/types": "5.48.1",
|
||||||
"@typescript-eslint/typescript-estree": "5.45.0",
|
"@typescript-eslint/typescript-estree": "5.48.1",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -4388,13 +4378,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.48.1.tgz",
|
||||||
"integrity": "sha512-noDMjr87Arp/PuVrtvN3dXiJstQR1+XlQ4R1EvzG+NMgXi8CuMCXpb8JqNtFHKceVSQ985BZhfRdowJzbv4yKw==",
|
"integrity": "sha512-S035ueRrbxRMKvSTv9vJKIWgr86BD8s3RqoRZmsSh/s8HhIs90g6UlK8ZabUSjUZQkhVxt7nmZ63VJ9dcZhtDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "5.45.0",
|
"@typescript-eslint/types": "5.48.1",
|
||||||
"@typescript-eslint/visitor-keys": "5.45.0"
|
"@typescript-eslint/visitor-keys": "5.48.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
@ -4405,13 +4395,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.48.1.tgz",
|
||||||
"integrity": "sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q==",
|
"integrity": "sha512-Hyr8HU8Alcuva1ppmqSYtM/Gp0q4JOp1F+/JH5D1IZm/bUBrV0edoewQZiEc1r6I8L4JL21broddxK8HAcZiqQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "5.45.0",
|
"@typescript-eslint/typescript-estree": "5.48.1",
|
||||||
"@typescript-eslint/utils": "5.45.0",
|
"@typescript-eslint/utils": "5.48.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"tsutils": "^3.21.0"
|
"tsutils": "^3.21.0"
|
||||||
},
|
},
|
||||||
@ -4449,9 +4439,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.48.1.tgz",
|
||||||
"integrity": "sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA==",
|
"integrity": "sha512-xHyDLU6MSuEEdIlzrrAerCGS3T7AA/L8Hggd0RCYBi0w3JMvGYxlLlXHeg50JI9Tfg5MrtsfuNxbS/3zF1/ATg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
@ -4462,13 +4452,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.1.tgz",
|
||||||
"integrity": "sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ==",
|
"integrity": "sha512-Hut+Osk5FYr+sgFh8J/FHjqX6HFcDzTlWLrFqGoK5kVUN3VBHF/QzZmAsIXCQ8T/W9nQNBTqalxi1P3LSqWnRA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "5.45.0",
|
"@typescript-eslint/types": "5.48.1",
|
||||||
"@typescript-eslint/visitor-keys": "5.45.0",
|
"@typescript-eslint/visitor-keys": "5.48.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"globby": "^11.1.0",
|
"globby": "^11.1.0",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
@ -4535,16 +4525,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.48.1.tgz",
|
||||||
"integrity": "sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA==",
|
"integrity": "sha512-SmQuSrCGUOdmGMwivW14Z0Lj8dxG1mOFZ7soeJ0TQZEJcs3n5Ndgkg0A4bcMFzBELqLJ6GTHnEU+iIoaD6hFGA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/json-schema": "^7.0.9",
|
"@types/json-schema": "^7.0.9",
|
||||||
"@types/semver": "^7.3.12",
|
"@types/semver": "^7.3.12",
|
||||||
"@typescript-eslint/scope-manager": "5.45.0",
|
"@typescript-eslint/scope-manager": "5.48.1",
|
||||||
"@typescript-eslint/types": "5.45.0",
|
"@typescript-eslint/types": "5.48.1",
|
||||||
"@typescript-eslint/typescript-estree": "5.45.0",
|
"@typescript-eslint/typescript-estree": "5.48.1",
|
||||||
"eslint-scope": "^5.1.1",
|
"eslint-scope": "^5.1.1",
|
||||||
"eslint-utils": "^3.0.0",
|
"eslint-utils": "^3.0.0",
|
||||||
"semver": "^7.3.7"
|
"semver": "^7.3.7"
|
||||||
@ -4561,12 +4551,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.1.tgz",
|
||||||
"integrity": "sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg==",
|
"integrity": "sha512-Ns0XBwmfuX7ZknznfXozgnydyR8F6ev/KEGePP4i74uL3ArsKbEhJ7raeKr1JSa997DBDwol/4a0Y+At82c9dA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "5.45.0",
|
"@typescript-eslint/types": "5.48.1",
|
||||||
"eslint-visitor-keys": "^3.3.0"
|
"eslint-visitor-keys": "^3.3.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -7020,9 +7010,9 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/cypress": {
|
"node_modules/cypress": {
|
||||||
"version": "12.1.0",
|
"version": "12.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.3.0.tgz",
|
||||||
"integrity": "sha512-7fz8N84uhN1+ePNDsfQvoWEl4P3/VGKKmAg+bJQFY4onhA37Ys+6oBkGbNdwGeC7n2QqibNVPhk8x3YuQLwzfw==",
|
"integrity": "sha512-ZQNebibi6NBt51TRxRMYKeFvIiQZ01t50HSy7z/JMgRVqBUey3cdjog5MYEbzG6Ktti5ckDt1tfcC47lmFwXkw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -7670,12 +7660,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/echarts": {
|
"node_modules/echarts": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.1.tgz",
|
||||||
"integrity": "sha512-uPsO9VRUIKAdFOoH3B0aNg7NRVdN7aM39/OjovjO9MwmWsAkfGyeXJhK+dbRi51iDrQWliXV60/XwLA7kg3z0w==",
|
"integrity": "sha512-9ltS3M2JB0w2EhcYjCdmtrJ+6haZcW6acBolMGIuf01Hql1yrIV01L1aRj7jsaaIULJslEP9Z3vKlEmnJaWJVQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "2.3.0",
|
"tslib": "2.3.0",
|
||||||
"zrender": "5.4.0"
|
"zrender": "5.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/echarts-gl": {
|
"node_modules/echarts-gl": {
|
||||||
@ -8425,13 +8415,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "8.28.0",
|
"version": "8.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.31.0.tgz",
|
||||||
"integrity": "sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ==",
|
"integrity": "sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/eslintrc": "^1.3.3",
|
"@eslint/eslintrc": "^1.4.1",
|
||||||
"@humanwhocodes/config-array": "^0.11.6",
|
"@humanwhocodes/config-array": "^0.11.8",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
"@nodelib/fs.walk": "^1.2.8",
|
"@nodelib/fs.walk": "^1.2.8",
|
||||||
"ajv": "^6.10.0",
|
"ajv": "^6.10.0",
|
||||||
@ -8450,7 +8440,7 @@
|
|||||||
"file-entry-cache": "^6.0.1",
|
"file-entry-cache": "^6.0.1",
|
||||||
"find-up": "^5.0.0",
|
"find-up": "^5.0.0",
|
||||||
"glob-parent": "^6.0.2",
|
"glob-parent": "^6.0.2",
|
||||||
"globals": "^13.15.0",
|
"globals": "^13.19.0",
|
||||||
"grapheme-splitter": "^1.0.4",
|
"grapheme-splitter": "^1.0.4",
|
||||||
"ignore": "^5.2.0",
|
"ignore": "^5.2.0",
|
||||||
"import-fresh": "^3.0.0",
|
"import-fresh": "^3.0.0",
|
||||||
@ -8646,9 +8636,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint/node_modules/globals": {
|
"node_modules/eslint/node_modules/globals": {
|
||||||
"version": "13.16.0",
|
"version": "13.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz",
|
||||||
"integrity": "sha512-A1lrQfpNF+McdPOnnFqY3kSN0AFTy485bTi1bkLk4mVPODIUEcSfhHgRqA+QdXPksrSTTztYXx37NFV+GpGk3Q==",
|
"integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"type-fest": "^0.20.2"
|
"type-fest": "^0.20.2"
|
||||||
@ -10997,9 +10987,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz",
|
||||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
|
"integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"json5": "lib/cli.js"
|
"json5": "lib/cli.js"
|
||||||
},
|
},
|
||||||
@ -14062,9 +14052,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "2.8.0",
|
"version": "2.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.2.tgz",
|
||||||
"integrity": "sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==",
|
"integrity": "sha512-BtRV9BcncDyI2tsuS19zzhzoxD8Dh8LiCx7j7tHzrkz8GFXAexeWFdi22mjE1d16dftH2qNaytVxqiRTGlMfpw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin-prettier.js"
|
"prettier": "bin-prettier.js"
|
||||||
@ -14270,6 +14260,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
|
||||||
|
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/querystring": {
|
"node_modules/querystring": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||||
@ -14741,9 +14740,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rxjs": {
|
"node_modules/rxjs": {
|
||||||
"version": "7.5.7",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz",
|
||||||
"integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==",
|
"integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
@ -17236,9 +17235,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zrender": {
|
"node_modules/zrender": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.1.tgz",
|
||||||
"integrity": "sha512-rOS09Z2HSVGFs2dn/TuYk5BlCaZcVe8UDLLjj1ySYF828LATKKdxuakSZMvrDz54yiKPDYVfjdKqcX8Jky3BIA==",
|
"integrity": "sha512-M4Z05BHWtajY2241EmMPHglDQAJ1UyHQcYsxDNzD9XLSkPDqMq4bB28v9Pb4mvHnVQ0GxyTklZ/69xCFP6RXBA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "2.3.0"
|
"tslib": "2.3.0"
|
||||||
}
|
}
|
||||||
@ -19327,12 +19326,6 @@
|
|||||||
"verror": "1.10.0"
|
"verror": "1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"qs": {
|
|
||||||
"version": "6.5.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
|
||||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"tough-cookie": {
|
"tough-cookie": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||||
@ -19462,15 +19455,15 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"@eslint/eslintrc": {
|
"@eslint/eslintrc": {
|
||||||
"version": "1.3.3",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz",
|
||||||
"integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==",
|
"integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ajv": "^6.12.4",
|
"ajv": "^6.12.4",
|
||||||
"debug": "^4.3.2",
|
"debug": "^4.3.2",
|
||||||
"espree": "^9.4.0",
|
"espree": "^9.4.0",
|
||||||
"globals": "^13.15.0",
|
"globals": "^13.19.0",
|
||||||
"ignore": "^5.2.0",
|
"ignore": "^5.2.0",
|
||||||
"import-fresh": "^3.2.1",
|
"import-fresh": "^3.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
@ -19485,9 +19478,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"version": "13.18.0",
|
"version": "13.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz",
|
||||||
"integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==",
|
"integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"type-fest": "^0.20.2"
|
"type-fest": "^0.20.2"
|
||||||
@ -19608,9 +19601,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@humanwhocodes/config-array": {
|
"@humanwhocodes/config-array": {
|
||||||
"version": "0.11.7",
|
"version": "0.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
|
||||||
"integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==",
|
"integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@humanwhocodes/object-schema": "^1.2.1",
|
"@humanwhocodes/object-schema": "^1.2.1",
|
||||||
@ -20216,14 +20209,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/eslint-plugin": {
|
"@typescript-eslint/eslint-plugin": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.48.1.tgz",
|
||||||
"integrity": "sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA==",
|
"integrity": "sha512-9nY5K1Rp2ppmpb9s9S2aBiF3xo5uExCehMDmYmmFqqyxgenbHJ3qbarcLt4ITgaD6r/2ypdlcFRdcuVPnks+fQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/scope-manager": "5.45.0",
|
"@typescript-eslint/scope-manager": "5.48.1",
|
||||||
"@typescript-eslint/type-utils": "5.45.0",
|
"@typescript-eslint/type-utils": "5.48.1",
|
||||||
"@typescript-eslint/utils": "5.45.0",
|
"@typescript-eslint/utils": "5.48.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ignore": "^5.2.0",
|
"ignore": "^5.2.0",
|
||||||
"natural-compare-lite": "^1.4.0",
|
"natural-compare-lite": "^1.4.0",
|
||||||
@ -20244,14 +20237,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/parser": {
|
"@typescript-eslint/parser": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.48.1.tgz",
|
||||||
"integrity": "sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ==",
|
"integrity": "sha512-4yg+FJR/V1M9Xoq56SF9Iygqm+r5LMXvheo6DQ7/yUWynQ4YfCRnsKuRgqH4EQ5Ya76rVwlEpw4Xu+TgWQUcdA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/scope-manager": "5.45.0",
|
"@typescript-eslint/scope-manager": "5.48.1",
|
||||||
"@typescript-eslint/types": "5.45.0",
|
"@typescript-eslint/types": "5.48.1",
|
||||||
"@typescript-eslint/typescript-estree": "5.45.0",
|
"@typescript-eslint/typescript-estree": "5.48.1",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -20267,23 +20260,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/scope-manager": {
|
"@typescript-eslint/scope-manager": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.48.1.tgz",
|
||||||
"integrity": "sha512-noDMjr87Arp/PuVrtvN3dXiJstQR1+XlQ4R1EvzG+NMgXi8CuMCXpb8JqNtFHKceVSQ985BZhfRdowJzbv4yKw==",
|
"integrity": "sha512-S035ueRrbxRMKvSTv9vJKIWgr86BD8s3RqoRZmsSh/s8HhIs90g6UlK8ZabUSjUZQkhVxt7nmZ63VJ9dcZhtDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/types": "5.45.0",
|
"@typescript-eslint/types": "5.48.1",
|
||||||
"@typescript-eslint/visitor-keys": "5.45.0"
|
"@typescript-eslint/visitor-keys": "5.48.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/type-utils": {
|
"@typescript-eslint/type-utils": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.48.1.tgz",
|
||||||
"integrity": "sha512-DY7BXVFSIGRGFZ574hTEyLPRiQIvI/9oGcN8t1A7f6zIs6ftbrU0nhyV26ZW//6f85avkwrLag424n+fkuoJ1Q==",
|
"integrity": "sha512-Hyr8HU8Alcuva1ppmqSYtM/Gp0q4JOp1F+/JH5D1IZm/bUBrV0edoewQZiEc1r6I8L4JL21broddxK8HAcZiqQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/typescript-estree": "5.45.0",
|
"@typescript-eslint/typescript-estree": "5.48.1",
|
||||||
"@typescript-eslint/utils": "5.45.0",
|
"@typescript-eslint/utils": "5.48.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"tsutils": "^3.21.0"
|
"tsutils": "^3.21.0"
|
||||||
},
|
},
|
||||||
@ -20300,19 +20293,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/types": {
|
"@typescript-eslint/types": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.48.1.tgz",
|
||||||
"integrity": "sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA==",
|
"integrity": "sha512-xHyDLU6MSuEEdIlzrrAerCGS3T7AA/L8Hggd0RCYBi0w3JMvGYxlLlXHeg50JI9Tfg5MrtsfuNxbS/3zF1/ATg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@typescript-eslint/typescript-estree": {
|
"@typescript-eslint/typescript-estree": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.1.tgz",
|
||||||
"integrity": "sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ==",
|
"integrity": "sha512-Hut+Osk5FYr+sgFh8J/FHjqX6HFcDzTlWLrFqGoK5kVUN3VBHF/QzZmAsIXCQ8T/W9nQNBTqalxi1P3LSqWnRA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/types": "5.45.0",
|
"@typescript-eslint/types": "5.48.1",
|
||||||
"@typescript-eslint/visitor-keys": "5.45.0",
|
"@typescript-eslint/visitor-keys": "5.48.1",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"globby": "^11.1.0",
|
"globby": "^11.1.0",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
@ -20352,28 +20345,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/utils": {
|
"@typescript-eslint/utils": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.48.1.tgz",
|
||||||
"integrity": "sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA==",
|
"integrity": "sha512-SmQuSrCGUOdmGMwivW14Z0Lj8dxG1mOFZ7soeJ0TQZEJcs3n5Ndgkg0A4bcMFzBELqLJ6GTHnEU+iIoaD6hFGA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/json-schema": "^7.0.9",
|
"@types/json-schema": "^7.0.9",
|
||||||
"@types/semver": "^7.3.12",
|
"@types/semver": "^7.3.12",
|
||||||
"@typescript-eslint/scope-manager": "5.45.0",
|
"@typescript-eslint/scope-manager": "5.48.1",
|
||||||
"@typescript-eslint/types": "5.45.0",
|
"@typescript-eslint/types": "5.48.1",
|
||||||
"@typescript-eslint/typescript-estree": "5.45.0",
|
"@typescript-eslint/typescript-estree": "5.48.1",
|
||||||
"eslint-scope": "^5.1.1",
|
"eslint-scope": "^5.1.1",
|
||||||
"eslint-utils": "^3.0.0",
|
"eslint-utils": "^3.0.0",
|
||||||
"semver": "^7.3.7"
|
"semver": "^7.3.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@typescript-eslint/visitor-keys": {
|
"@typescript-eslint/visitor-keys": {
|
||||||
"version": "5.45.0",
|
"version": "5.48.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.1.tgz",
|
||||||
"integrity": "sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg==",
|
"integrity": "sha512-Ns0XBwmfuX7ZknznfXozgnydyR8F6ev/KEGePP4i74uL3ArsKbEhJ7raeKr1JSa997DBDwol/4a0Y+At82c9dA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@typescript-eslint/types": "5.45.0",
|
"@typescript-eslint/types": "5.48.1",
|
||||||
"eslint-visitor-keys": "^3.3.0"
|
"eslint-visitor-keys": "^3.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -22283,9 +22276,9 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"cypress": {
|
"cypress": {
|
||||||
"version": "12.1.0",
|
"version": "12.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.3.0.tgz",
|
||||||
"integrity": "sha512-7fz8N84uhN1+ePNDsfQvoWEl4P3/VGKKmAg+bJQFY4onhA37Ys+6oBkGbNdwGeC7n2QqibNVPhk8x3YuQLwzfw==",
|
"integrity": "sha512-ZQNebibi6NBt51TRxRMYKeFvIiQZ01t50HSy7z/JMgRVqBUey3cdjog5MYEbzG6Ktti5ckDt1tfcC47lmFwXkw==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@cypress/request": "^2.88.10",
|
"@cypress/request": "^2.88.10",
|
||||||
@ -22797,12 +22790,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"echarts": {
|
"echarts": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.4.1.tgz",
|
||||||
"integrity": "sha512-uPsO9VRUIKAdFOoH3B0aNg7NRVdN7aM39/OjovjO9MwmWsAkfGyeXJhK+dbRi51iDrQWliXV60/XwLA7kg3z0w==",
|
"integrity": "sha512-9ltS3M2JB0w2EhcYjCdmtrJ+6haZcW6acBolMGIuf01Hql1yrIV01L1aRj7jsaaIULJslEP9Z3vKlEmnJaWJVQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tslib": "2.3.0",
|
"tslib": "2.3.0",
|
||||||
"zrender": "5.4.0"
|
"zrender": "5.4.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": {
|
"tslib": {
|
||||||
@ -23298,13 +23291,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"eslint": {
|
"eslint": {
|
||||||
"version": "8.28.0",
|
"version": "8.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.31.0.tgz",
|
||||||
"integrity": "sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ==",
|
"integrity": "sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@eslint/eslintrc": "^1.3.3",
|
"@eslint/eslintrc": "^1.4.1",
|
||||||
"@humanwhocodes/config-array": "^0.11.6",
|
"@humanwhocodes/config-array": "^0.11.8",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
"@nodelib/fs.walk": "^1.2.8",
|
"@nodelib/fs.walk": "^1.2.8",
|
||||||
"ajv": "^6.10.0",
|
"ajv": "^6.10.0",
|
||||||
@ -23323,7 +23316,7 @@
|
|||||||
"file-entry-cache": "^6.0.1",
|
"file-entry-cache": "^6.0.1",
|
||||||
"find-up": "^5.0.0",
|
"find-up": "^5.0.0",
|
||||||
"glob-parent": "^6.0.2",
|
"glob-parent": "^6.0.2",
|
||||||
"globals": "^13.15.0",
|
"globals": "^13.19.0",
|
||||||
"grapheme-splitter": "^1.0.4",
|
"grapheme-splitter": "^1.0.4",
|
||||||
"ignore": "^5.2.0",
|
"ignore": "^5.2.0",
|
||||||
"import-fresh": "^3.0.0",
|
"import-fresh": "^3.0.0",
|
||||||
@ -23426,9 +23419,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"globals": {
|
"globals": {
|
||||||
"version": "13.16.0",
|
"version": "13.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz",
|
||||||
"integrity": "sha512-A1lrQfpNF+McdPOnnFqY3kSN0AFTy485bTi1bkLk4mVPODIUEcSfhHgRqA+QdXPksrSTTztYXx37NFV+GpGk3Q==",
|
"integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"type-fest": "^0.20.2"
|
"type-fest": "^0.20.2"
|
||||||
@ -25197,9 +25190,9 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"json5": {
|
"json5": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz",
|
||||||
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
|
"integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ=="
|
||||||
},
|
},
|
||||||
"jsonfile": {
|
"jsonfile": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
@ -27381,9 +27374,9 @@
|
|||||||
"integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ="
|
"integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ="
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"version": "2.8.0",
|
"version": "2.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.2.tgz",
|
||||||
"integrity": "sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==",
|
"integrity": "sha512-BtRV9BcncDyI2tsuS19zzhzoxD8Dh8LiCx7j7tHzrkz8GFXAexeWFdi22mjE1d16dftH2qNaytVxqiRTGlMfpw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"pretty-bytes": {
|
"pretty-bytes": {
|
||||||
@ -27541,6 +27534,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"qs": {
|
||||||
|
"version": "6.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
|
||||||
|
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"querystring": {
|
"querystring": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||||
@ -27894,9 +27893,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rxjs": {
|
"rxjs": {
|
||||||
"version": "7.5.7",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz",
|
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz",
|
||||||
"integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==",
|
"integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
@ -29739,9 +29738,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zrender": {
|
"zrender": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.4.1.tgz",
|
||||||
"integrity": "sha512-rOS09Z2HSVGFs2dn/TuYk5BlCaZcVe8UDLLjj1ySYF828LATKKdxuakSZMvrDz54yiKPDYVfjdKqcX8Jky3BIA==",
|
"integrity": "sha512-M4Z05BHWtajY2241EmMPHglDQAJ1UyHQcYsxDNzD9XLSkPDqMq4bB28v9Pb4mvHnVQ0GxyTklZ/69xCFP6RXBA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tslib": "2.3.0"
|
"tslib": "2.3.0"
|
||||||
},
|
},
|
||||||
|
@ -84,13 +84,13 @@
|
|||||||
"browserify": "^17.0.0",
|
"browserify": "^17.0.0",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"domino": "^2.1.6",
|
"domino": "^2.1.6",
|
||||||
"echarts": "~5.4.0",
|
"echarts": "~5.4.1",
|
||||||
"echarts-gl": "^2.0.9",
|
"echarts-gl": "^2.0.9",
|
||||||
"lightweight-charts": "~3.8.0",
|
"lightweight-charts": "~3.8.0",
|
||||||
"ngx-echarts": "~14.0.0",
|
"ngx-echarts": "~14.0.0",
|
||||||
"ngx-infinite-scroll": "^14.0.1",
|
"ngx-infinite-scroll": "^14.0.1",
|
||||||
"qrcode": "1.5.1",
|
"qrcode": "1.5.1",
|
||||||
"rxjs": "~7.5.7",
|
"rxjs": "~7.8.0",
|
||||||
"tinyify": "^3.1.0",
|
"tinyify": "^3.1.0",
|
||||||
"tlite": "^0.1.9",
|
"tlite": "^0.1.9",
|
||||||
"tslib": "~2.4.1",
|
"tslib": "~2.4.1",
|
||||||
@ -100,17 +100,17 @@
|
|||||||
"@angular/compiler-cli": "^14.2.12",
|
"@angular/compiler-cli": "^14.2.12",
|
||||||
"@angular/language-service": "^14.2.12",
|
"@angular/language-service": "^14.2.12",
|
||||||
"@types/node": "^18.11.9",
|
"@types/node": "^18.11.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.48.1",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.31.0",
|
||||||
"http-proxy-middleware": "~2.0.6",
|
"http-proxy-middleware": "~2.0.6",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.2",
|
||||||
"ts-node": "~10.9.1",
|
"ts-node": "~10.9.1",
|
||||||
"typescript": "~4.6.4"
|
"typescript": "~4.6.4"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@cypress/schematic": "^2.4.0",
|
"@cypress/schematic": "^2.4.0",
|
||||||
"cypress": "^12.1.0",
|
"cypress": "^12.3.0",
|
||||||
"cypress-fail-on-console-error": "~4.0.2",
|
"cypress-fail-on-console-error": "~4.0.2",
|
||||||
"cypress-wait-until": "^1.7.2",
|
"cypress-wait-until": "^1.7.2",
|
||||||
"mock-socket": "~9.1.5",
|
"mock-socket": "~9.1.5",
|
||||||
|
@ -76,7 +76,7 @@ PROXY_CONFIG = [
|
|||||||
|
|
||||||
if (configContent && configContent.BASE_MODULE == "liquid") {
|
if (configContent && configContent.BASE_MODULE == "liquid") {
|
||||||
PROXY_CONFIG.push({
|
PROXY_CONFIG.push({
|
||||||
context: ['/resources/pools.json',
|
context: [
|
||||||
'/resources/assets.json', '/resources/assets.minimal.json',
|
'/resources/assets.json', '/resources/assets.minimal.json',
|
||||||
'/resources/assets-testnet.json', '/resources/assets-testnet.minimal.json'],
|
'/resources/assets-testnet.json', '/resources/assets-testnet.minimal.json'],
|
||||||
target: "https://liquid.network",
|
target: "https://liquid.network",
|
||||||
@ -85,7 +85,7 @@ if (configContent && configContent.BASE_MODULE == "liquid") {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
PROXY_CONFIG.push({
|
PROXY_CONFIG.push({
|
||||||
context: ['/resources/pools.json', '/resources/assets.json', '/resources/assets.minimal.json', '/resources/worldmap.json'],
|
context: ['/resources/assets.json', '/resources/assets.minimal.json', '/resources/worldmap.json'],
|
||||||
target: "https://mempool.space",
|
target: "https://mempool.space",
|
||||||
secure: false,
|
secure: false,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
@ -3,8 +3,8 @@ const fs = require('fs');
|
|||||||
let PROXY_CONFIG = require('./proxy.conf');
|
let PROXY_CONFIG = require('./proxy.conf');
|
||||||
|
|
||||||
PROXY_CONFIG.forEach(entry => {
|
PROXY_CONFIG.forEach(entry => {
|
||||||
entry.target = entry.target.replace("mempool.space", "mempool-staging.tk7.mempool.space");
|
entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
|
||||||
entry.target = entry.target.replace("liquid.network", "liquid-staging.tk7.mempool.space");
|
entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
|
||||||
entry.target = entry.target.replace("bisq.markets", "bisq-staging.fra.mempool.space");
|
entry.target = entry.target.replace("bisq.markets", "bisq-staging.fra.mempool.space");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -116,11 +116,12 @@ export const languages: Language[] = [
|
|||||||
// { code: 'hr', name: 'Hrvatski' }, // Croatian
|
// { code: 'hr', name: 'Hrvatski' }, // Croatian
|
||||||
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
|
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
|
||||||
{ code: 'hi', name: 'हिन्दी' }, // Hindi
|
{ code: 'hi', name: 'हिन्दी' }, // Hindi
|
||||||
|
{ code: 'ne', name: 'नेपाली' }, // Nepalese
|
||||||
{ code: 'it', name: 'Italiano' }, // Italian
|
{ code: 'it', name: 'Italiano' }, // Italian
|
||||||
{ code: 'he', name: 'עברית' }, // Hebrew
|
{ code: 'he', name: 'עברית' }, // Hebrew
|
||||||
{ code: 'ka', name: 'ქართული' }, // Georgian
|
{ code: 'ka', name: 'ქართული' }, // Georgian
|
||||||
// { code: 'lv', name: 'Latviešu' }, // Latvian
|
// { code: 'lv', name: 'Latviešu' }, // Latvian
|
||||||
// { code: 'lt', name: 'Lietuvių' }, // Lithuanian
|
{ code: 'lt', name: 'Lietuvių' }, // Lithuanian
|
||||||
{ code: 'hu', name: 'Magyar' }, // Hungarian
|
{ code: 'hu', name: 'Magyar' }, // Hungarian
|
||||||
{ code: 'mk', name: 'Македонски' }, // Macedonian
|
{ code: 'mk', name: 'Македонски' }, // Macedonian
|
||||||
// { code: 'ms', name: 'Bahasa Melayu' }, // Malay
|
// { code: 'ms', name: 'Bahasa Melayu' }, // Malay
|
||||||
@ -156,3 +157,41 @@ export const specialBlocks = {
|
|||||||
labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
|
labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fiatCurrencies = {
|
||||||
|
AUD: {
|
||||||
|
name: 'Australian Dollar',
|
||||||
|
code: 'AUD',
|
||||||
|
indexed: true,
|
||||||
|
},
|
||||||
|
CAD: {
|
||||||
|
name: 'Canadian Dollar',
|
||||||
|
code: 'CAD',
|
||||||
|
indexed: true,
|
||||||
|
},
|
||||||
|
CHF: {
|
||||||
|
name: 'Swiss Franc',
|
||||||
|
code: 'CHF',
|
||||||
|
indexed: true,
|
||||||
|
},
|
||||||
|
EUR: {
|
||||||
|
name: 'Euro',
|
||||||
|
code: 'EUR',
|
||||||
|
indexed: true,
|
||||||
|
},
|
||||||
|
GBP: {
|
||||||
|
name: 'Pound Sterling',
|
||||||
|
code: 'GBP',
|
||||||
|
indexed: true,
|
||||||
|
},
|
||||||
|
JPY: {
|
||||||
|
name: 'Japanese Yen',
|
||||||
|
code: 'JPY',
|
||||||
|
indexed: true,
|
||||||
|
},
|
||||||
|
USD: {
|
||||||
|
name: 'US Dollar',
|
||||||
|
code: 'USD',
|
||||||
|
indexed: true,
|
||||||
|
},
|
||||||
|
};
|
@ -6,6 +6,7 @@ import { AppRoutingModule } from './app-routing.module';
|
|||||||
import { AppComponent } from './components/app/app.component';
|
import { AppComponent } from './components/app/app.component';
|
||||||
import { ElectrsApiService } from './services/electrs-api.service';
|
import { ElectrsApiService } from './services/electrs-api.service';
|
||||||
import { StateService } from './services/state.service';
|
import { StateService } from './services/state.service';
|
||||||
|
import { CacheService } from './services/cache.service';
|
||||||
import { EnterpriseService } from './services/enterprise.service';
|
import { EnterpriseService } from './services/enterprise.service';
|
||||||
import { WebsocketService } from './services/websocket.service';
|
import { WebsocketService } from './services/websocket.service';
|
||||||
import { AudioService } from './services/audio.service';
|
import { AudioService } from './services/audio.service';
|
||||||
@ -16,6 +17,7 @@ import { StorageService } from './services/storage.service';
|
|||||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||||
import { LanguageService } from './services/language.service';
|
import { LanguageService } from './services/language.service';
|
||||||
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
|
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
|
||||||
|
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
|
||||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||||
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
|
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
|
||||||
import { AppPreloadingStrategy } from './app.preloading-strategy';
|
import { AppPreloadingStrategy } from './app.preloading-strategy';
|
||||||
@ -23,6 +25,7 @@ import { AppPreloadingStrategy } from './app.preloading-strategy';
|
|||||||
const providers = [
|
const providers = [
|
||||||
ElectrsApiService,
|
ElectrsApiService,
|
||||||
StateService,
|
StateService,
|
||||||
|
CacheService,
|
||||||
WebsocketService,
|
WebsocketService,
|
||||||
AudioService,
|
AudioService,
|
||||||
SeoService,
|
SeoService,
|
||||||
@ -32,6 +35,7 @@ const providers = [
|
|||||||
LanguageService,
|
LanguageService,
|
||||||
ShortenStringPipe,
|
ShortenStringPipe,
|
||||||
FiatShortenerPipe,
|
FiatShortenerPipe,
|
||||||
|
FiatCurrencyPipe,
|
||||||
CapAddressPipe,
|
CapAddressPipe,
|
||||||
AppPreloadingStrategy,
|
AppPreloadingStrategy,
|
||||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
|
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
<div class="container-xl">
|
<div class="container-xl">
|
||||||
<h1 i18n="shared.address">Address</h1>
|
<h1 i18n="shared.address">Address</h1>
|
||||||
<span class="address-link">
|
<span class="address-link">
|
||||||
<a [routerLink]="['/address/' | relativeUrl, addressString]">
|
<app-truncate [text]="addressString" [lastChars]="8" [link]="['/address/' | relativeUrl, addressString]">
|
||||||
<span class="d-inline d-lg-none">{{ addressString | shortenString : 24 }}</span>
|
<app-clipboard [text]="addressString"></app-clipboard>
|
||||||
<span class="d-none d-lg-inline">{{ addressString }}</span>
|
</app-truncate>
|
||||||
</a>
|
|
||||||
<app-clipboard [text]="addressString"></app-clipboard>
|
|
||||||
</span>
|
</span>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
@ -27,6 +27,9 @@
|
|||||||
|
|
||||||
<br>
|
<br>
|
||||||
<ngb-pagination *ngIf="blocks.value" class="pagination-container" [size]="paginationSize" [collectionSize]="blocks.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
|
<ngb-pagination *ngIf="blocks.value" class="pagination-container" [size]="paginationSize" [collectionSize]="blocks.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
|
||||||
|
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
<br>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -6,12 +6,12 @@
|
|||||||
<h1 i18n="shared.transaction">Transaction</h1>
|
<h1 i18n="shared.transaction">Transaction</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="tx-link float-left">
|
<span class="tx-link">
|
||||||
<a [routerLink]="['/tx' | relativeUrl, bisqTx.id]">
|
<span class="txid">
|
||||||
<span class="d-inline d-lg-none">{{ bisqTx.id | shortenString : 24 }}</span>
|
<app-truncate [text]="bisqTx.id" [lastChars]="12" [link]="['/tx/' | relativeUrl, bisqTx.id]">
|
||||||
<span class="d-none d-lg-inline">{{ bisqTx.id }}</span>
|
<app-clipboard [text]="bisqTx.id"></app-clipboard>
|
||||||
</a>
|
</app-truncate>
|
||||||
<app-clipboard [text]="bisqTx.id"></app-clipboard>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="grow"></span>
|
<span class="grow"></span>
|
||||||
<div class="container-buttons">
|
<div class="container-buttons">
|
||||||
|
@ -68,8 +68,8 @@
|
|||||||
|
|
||||||
.community-sponsor {
|
.community-sponsor {
|
||||||
img {
|
img {
|
||||||
width: 67px;
|
width: 57px;
|
||||||
height: 67px;
|
height: 57px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,9 @@ export class AddressLabelsComponent implements OnChanges {
|
|||||||
|
|
||||||
handleChannel() {
|
handleChannel() {
|
||||||
const type = this.vout ? 'open' : 'close';
|
const type = this.vout ? 'open' : 'close';
|
||||||
this.label = `Channel ${type}: ${this.channel.node_left.alias} <> ${this.channel.node_right.alias}`;
|
const leftNodeName = this.channel.node_left.alias || this.channel.node_left.public_key.substring(0, 10);
|
||||||
|
const rightNodeName = this.channel.node_right.alias || this.channel.node_right.public_key.substring(0, 10);
|
||||||
|
this.label = `Channel ${type}: ${leftNodeName} <> ${rightNodeName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleVin() {
|
handleVin() {
|
||||||
|
@ -6,17 +6,16 @@
|
|||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
<div class="row d-flex justify-content-between">
|
<div class="row d-flex justify-content-between">
|
||||||
<div class="title-wrapper">
|
<div class="title-wrapper">
|
||||||
<h1 class="title truncated"><span class="first">{{addressString.slice(0,-4)}}</span><span class="last-four">{{addressString.slice(-4)}}</span></h1>
|
<h1 class="title"><app-truncate [text]="addressString"></app-truncate></h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngIf="addressInfo && addressInfo.unconfidential">
|
<tr *ngIf="addressInfo && addressInfo.unconfidential">
|
||||||
<td i18n="address.unconfidential">Unconfidential</td>
|
<td i18n="address.unconfidential">Unconfidential</td>
|
||||||
<td><a [routerLink]="['/address/' | relativeUrl, addressInfo.unconfidential]">
|
<td>
|
||||||
<span class="d-inline d-lg-none">{{ addressInfo.unconfidential | shortenString : 14 }}</span>
|
<app-truncate [text]="addressInfo.unconfidential" [lastChars]="7" [link]="['/address/' | relativeUrl, addressInfo.unconfidential]"></app-truncate>
|
||||||
<span class="d-none d-lg-inline">{{ addressInfo.unconfidential }}</span>
|
</td>
|
||||||
</a> <app-clipboard [text]="addressInfo.unconfidential"></app-clipboard></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<ng-template [ngIf]="!address.electrum">
|
<ng-template [ngIf]="!address.electrum">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -2,11 +2,9 @@
|
|||||||
<div class="title-address">
|
<div class="title-address">
|
||||||
<h1 i18n="shared.address">Address</h1>
|
<h1 i18n="shared.address">Address</h1>
|
||||||
<div class="tx-link">
|
<div class="tx-link">
|
||||||
<a [routerLink]="['/address/' | relativeUrl, addressString]" >
|
<app-truncate [text]="addressString" [lastChars]="8" [link]="['/address/' | relativeUrl, addressString]">
|
||||||
<span class="d-inline d-lg-none">{{ addressString | shortenString : 18 }}</span>
|
<app-clipboard [text]="addressString"></app-clipboard>
|
||||||
<span class="d-none d-lg-inline">{{ addressString }}</span>
|
</app-truncate>
|
||||||
</a>
|
|
||||||
<app-clipboard [text]="addressString"></app-clipboard>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -21,10 +19,11 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngIf="addressInfo && addressInfo.unconfidential">
|
<tr *ngIf="addressInfo && addressInfo.unconfidential">
|
||||||
<td i18n="address.unconfidential">Unconfidential</td>
|
<td i18n="address.unconfidential">Unconfidential</td>
|
||||||
<td><a [routerLink]="['/address/' | relativeUrl, addressInfo.unconfidential]">
|
<td>
|
||||||
<span class="d-inline d-lg-none">{{ addressInfo.unconfidential | shortenString : 14 }}</span>
|
<app-truncate [text]="addressInfo.unconfidential" [lastChars]="8" [link]="['/address/' | relativeUrl, addressInfo.unconfidential]">
|
||||||
<span class="d-none d-lg-inline">{{ addressInfo.unconfidential }}</span>
|
<app-clipboard [text]="addressInfo.unconfidential"></app-clipboard>
|
||||||
</a> <app-clipboard [text]="addressInfo.unconfidential"></app-clipboard></td>
|
</app-truncate>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<ng-template [ngIf]="!address.electrum">
|
<ng-template [ngIf]="!address.electrum">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
|
<ng-container *ngIf="!noFiat && (viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
|
||||||
<span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ conversions.USD * (satoshis / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
|
<span class="fiat">{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ (conversions ? conversions[currency] : 0) * satoshis / 100000000 | fiatCurrency : digitsInfo : currency }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #viewFiatVin>
|
<ng-template #viewFiatVin>
|
||||||
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && (satoshis === undefined || satoshis === null)" [ngIfElse]="default">
|
<ng-template [ngIf]="(network === 'liquid' || network === 'liquidtestnet') && (satoshis === undefined || satoshis === null)" [ngIfElse]="default">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy } from '@angular/core';
|
import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
|
|
||||||
@ -10,10 +10,12 @@ import { Observable, Subscription } from 'rxjs';
|
|||||||
})
|
})
|
||||||
export class AmountComponent implements OnInit, OnDestroy {
|
export class AmountComponent implements OnInit, OnDestroy {
|
||||||
conversions$: Observable<any>;
|
conversions$: Observable<any>;
|
||||||
|
currency: string;
|
||||||
viewFiat$: Observable<boolean>;
|
viewFiat$: Observable<boolean>;
|
||||||
network = '';
|
network = '';
|
||||||
|
|
||||||
stateSubscription: Subscription;
|
stateSubscription: Subscription;
|
||||||
|
currencySubscription: Subscription;
|
||||||
|
|
||||||
@Input() satoshis: number;
|
@Input() satoshis: number;
|
||||||
@Input() digitsInfo = '1.8-8';
|
@Input() digitsInfo = '1.8-8';
|
||||||
@ -22,7 +24,13 @@ export class AmountComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
) { }
|
private cd: ChangeDetectorRef,
|
||||||
|
) {
|
||||||
|
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||||
|
this.currency = fiat;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.viewFiat$ = this.stateService.viewFiat$.asObservable();
|
this.viewFiat$ = this.stateService.viewFiat$.asObservable();
|
||||||
@ -34,6 +42,7 @@ export class AmountComponent implements OnInit, OnDestroy {
|
|||||||
if (this.stateSubscription) {
|
if (this.stateSubscription) {
|
||||||
this.stateSubscription.unsubscribe();
|
this.stateSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
this.currencySubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,10 @@ export class AppComponent implements OnInit {
|
|||||||
if (event.target instanceof HTMLInputElement) {
|
if (event.target instanceof HTMLInputElement) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// prevent arrow key horizontal scrolling
|
||||||
|
if(["ArrowLeft","ArrowRight"].indexOf(event.code) > -1) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
this.stateService.keyNavigation$.next(event);
|
this.stateService.keyNavigation$.next(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,11 +2,9 @@
|
|||||||
<div class="title-asset">
|
<div class="title-asset">
|
||||||
<h1 i18n="asset|Liquid Asset page title">Asset</h1>
|
<h1 i18n="asset|Liquid Asset page title">Asset</h1>
|
||||||
<div class="tx-link">
|
<div class="tx-link">
|
||||||
<a [routerLink]="['/assets/asset/' | relativeUrl, assetString]">
|
<app-truncate [text]="assetString" [lastChars]="8" [link]="['/assets/asset/' | relativeUrl, assetString]">
|
||||||
<span class="d-inline d-lg-none">{{ assetString | shortenString : 24 }}</span>
|
<app-clipboard [text]="assetString"></app-clipboard>
|
||||||
<span class="d-none d-lg-inline">{{ assetString }}</span>
|
</app-truncate>
|
||||||
</a>
|
|
||||||
<app-clipboard [text]="assetString"></app-clipboard>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@
|
|||||||
|
|
||||||
<ngb-pagination [collectionSize]="assets.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="ellipses"></ngb-pagination>
|
<ngb-pagination [collectionSize]="assets.length" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="ellipses"></ngb-pagination>
|
||||||
|
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
<br>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-template #isLoading>
|
<ng-template #isLoading>
|
||||||
|
@ -23,7 +23,6 @@
|
|||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
height: calc(100% - 150px);
|
height: calc(100% - 150px);
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
height: 100%;
|
|
||||||
padding-bottom: 100px;
|
padding-bottom: 100px;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -213,6 +213,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
legend: (data.series.length === 0) ? undefined : {
|
legend: (data.series.length === 0) ? undefined : {
|
||||||
|
padding: [10, 75],
|
||||||
data: data.legends,
|
data: data.legends,
|
||||||
selected: JSON.parse(this.storageService.getValue('fee_rates_legend')) ?? {
|
selected: JSON.parse(this.storageService.getValue('fee_rates_legend')) ?? {
|
||||||
'Min': true,
|
'Min': true,
|
||||||
|
@ -23,7 +23,6 @@
|
|||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
height: calc(100% - 150px);
|
height: calc(100% - 150px);
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
height: 100%;
|
|
||||||
padding-bottom: 100px;
|
padding-bottom: 100px;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||||
import { EChartsOption, graphic } from 'echarts';
|
import { EChartsOption, graphic } from 'echarts';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
|
import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
|
||||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
||||||
|
import { StateService } from '../../services/state.service';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
||||||
|
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
||||||
|
import { fiatCurrencies } from '../../app.constants';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block-fees-graph',
|
selector: 'app-block-fees-graph',
|
||||||
@ -44,6 +47,9 @@ export class BlockFeesGraphComponent implements OnInit {
|
|||||||
timespan = '';
|
timespan = '';
|
||||||
chartInstance: any = undefined;
|
chartInstance: any = undefined;
|
||||||
|
|
||||||
|
currencySubscription: Subscription;
|
||||||
|
currency: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
@ -51,11 +57,21 @@ export class BlockFeesGraphComponent implements OnInit {
|
|||||||
private formBuilder: UntypedFormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
|
private stateService: StateService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private fiatShortenerPipe: FiatShortenerPipe,
|
private fiatShortenerPipe: FiatShortenerPipe,
|
||||||
|
private fiatCurrencyPipe: FiatCurrencyPipe,
|
||||||
) {
|
) {
|
||||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||||
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
||||||
|
|
||||||
|
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||||
|
if (fiat && fiatCurrencies[fiat]?.indexed) {
|
||||||
|
this.currency = fiat;
|
||||||
|
} else {
|
||||||
|
this.currency = 'USD';
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -84,7 +100,7 @@ export class BlockFeesGraphComponent implements OnInit {
|
|||||||
tap((response) => {
|
tap((response) => {
|
||||||
this.prepareChartOptions({
|
this.prepareChartOptions({
|
||||||
blockFees: response.body.map(val => [val.timestamp * 1000, val.avgFees / 100000000, val.avgHeight]),
|
blockFees: response.body.map(val => [val.timestamp * 1000, val.avgFees / 100000000, val.avgHeight]),
|
||||||
blockFeesUSD: response.body.filter(val => val.USD > 0).map(val => [val.timestamp * 1000, val.avgFees / 100000000 * val.USD, val.avgHeight]),
|
blockFeesFiat: response.body.filter(val => val[this.currency] > 0).map(val => [val.timestamp * 1000, val.avgFees / 100000000 * val[this.currency], val.avgHeight]),
|
||||||
});
|
});
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}),
|
}),
|
||||||
@ -157,7 +173,7 @@ export class BlockFeesGraphComponent implements OnInit {
|
|||||||
if (tick.seriesIndex === 0) {
|
if (tick.seriesIndex === 0) {
|
||||||
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.3-3')} BTC<br>`;
|
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.3-3')} BTC<br>`;
|
||||||
} else if (tick.seriesIndex === 1) {
|
} else if (tick.seriesIndex === 1) {
|
||||||
tooltip += `${tick.marker} ${tick.seriesName}: ${formatCurrency(tick.data[1], this.locale, getCurrencySymbol('USD', 'narrow'), 'USD', '1.0-0')}<br>`;
|
tooltip += `${tick.marker} ${tick.seriesName}: ${this.fiatCurrencyPipe.transform(tick.data[1], null, this.currency) }<br>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +200,7 @@ export class BlockFeesGraphComponent implements OnInit {
|
|||||||
icon: 'roundRect',
|
icon: 'roundRect',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Fees USD',
|
name: 'Fees ' + this.currency,
|
||||||
inactiveColor: 'rgb(110, 112, 121)',
|
inactiveColor: 'rgb(110, 112, 121)',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: 'white',
|
color: 'white',
|
||||||
@ -216,7 +232,7 @@ export class BlockFeesGraphComponent implements OnInit {
|
|||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: 'rgb(110, 112, 121)',
|
color: 'rgb(110, 112, 121)',
|
||||||
formatter: function(val) {
|
formatter: function(val) {
|
||||||
return this.fiatShortenerPipe.transform(val);
|
return this.fiatShortenerPipe.transform(val, null, this.currency);
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
@ -243,8 +259,8 @@ export class BlockFeesGraphComponent implements OnInit {
|
|||||||
legendHoverLink: false,
|
legendHoverLink: false,
|
||||||
zlevel: 1,
|
zlevel: 1,
|
||||||
yAxisIndex: 1,
|
yAxisIndex: 1,
|
||||||
name: 'Fees USD',
|
name: 'Fees ' + this.currency,
|
||||||
data: data.blockFeesUSD,
|
data: data.blockFeesFiat,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
smooth: 0.25,
|
smooth: 0.25,
|
||||||
symbol: 'none',
|
symbol: 'none',
|
||||||
|
@ -9,5 +9,6 @@
|
|||||||
[tx]="selectedTx || hoverTx"
|
[tx]="selectedTx || hoverTx"
|
||||||
[cursorPosition]="tooltipPosition"
|
[cursorPosition]="tooltipPosition"
|
||||||
[clickable]="!!selectedTx"
|
[clickable]="!!selectedTx"
|
||||||
|
[auditEnabled]="auditHighlighting"
|
||||||
></app-block-overview-tooltip>
|
></app-block-overview-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,6 +20,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
@Input() disableSpinner = false;
|
@Input() disableSpinner = false;
|
||||||
@Input() mirrorTxid: string | void;
|
@Input() mirrorTxid: string | void;
|
||||||
@Input() unavailable: boolean = false;
|
@Input() unavailable: boolean = false;
|
||||||
|
@Input() auditHighlighting: boolean = false;
|
||||||
@Output() txClickEvent = new EventEmitter<TransactionStripped>();
|
@Output() txClickEvent = new EventEmitter<TransactionStripped>();
|
||||||
@Output() txHoverEvent = new EventEmitter<string>();
|
@Output() txHoverEvent = new EventEmitter<string>();
|
||||||
@Output() readyEvent = new EventEmitter();
|
@Output() readyEvent = new EventEmitter();
|
||||||
@ -70,6 +71,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
if (changes.mirrorTxid) {
|
if (changes.mirrorTxid) {
|
||||||
this.setMirror(this.mirrorTxid);
|
this.setMirror(this.mirrorTxid);
|
||||||
}
|
}
|
||||||
|
if (changes.auditHighlighting) {
|
||||||
|
this.setHighlightingEnabled(this.auditHighlighting);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
@ -195,7 +199,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
this.start();
|
this.start();
|
||||||
} else {
|
} else {
|
||||||
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
||||||
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray });
|
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, highlighting: this.auditHighlighting });
|
||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -396,6 +400,13 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setHighlightingEnabled(enabled: boolean): void {
|
||||||
|
if (this.scene) {
|
||||||
|
this.scene.setHighlighting(enabled);
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onTxClick(cssX: number, cssY: number) {
|
onTxClick(cssX: number, cssY: number) {
|
||||||
const x = cssX * window.devicePixelRatio;
|
const x = cssX * window.devicePixelRatio;
|
||||||
const y = cssY * window.devicePixelRatio;
|
const y = cssY * window.devicePixelRatio;
|
||||||
|
@ -9,6 +9,7 @@ export default class BlockScene {
|
|||||||
txs: { [key: string]: TxView };
|
txs: { [key: string]: TxView };
|
||||||
orientation: string;
|
orientation: string;
|
||||||
flip: boolean;
|
flip: boolean;
|
||||||
|
highlightingEnabled: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
gridWidth: number;
|
gridWidth: number;
|
||||||
@ -22,11 +23,11 @@ export default class BlockScene {
|
|||||||
animateUntil = 0;
|
animateUntil = 0;
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
|
|
||||||
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray }:
|
constructor({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
|
||||||
{ width: number, height: number, resolution: number, blockLimit: number,
|
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||||
orientation: string, flip: boolean, vertexArray: FastVertexArray }
|
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
|
||||||
) {
|
) {
|
||||||
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray });
|
this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting });
|
||||||
}
|
}
|
||||||
|
|
||||||
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
|
resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void {
|
||||||
@ -51,6 +52,13 @@ export default class BlockScene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setHighlighting(enabled: boolean): void {
|
||||||
|
this.highlightingEnabled = enabled;
|
||||||
|
if (this.initialised && this.scene) {
|
||||||
|
this.updateAll(performance.now(), 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Destroy the current layout and clean up graphics sprites without any exit animation
|
// Destroy the current layout and clean up graphics sprites without any exit animation
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
Object.values(this.txs).forEach(tx => tx.destroy());
|
Object.values(this.txs).forEach(tx => tx.destroy());
|
||||||
@ -67,7 +75,7 @@ export default class BlockScene {
|
|||||||
});
|
});
|
||||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
||||||
txs.forEach(tx => {
|
txs.forEach(tx => {
|
||||||
const txView = new TxView(tx, this.vertexArray);
|
const txView = new TxView(tx, this);
|
||||||
this.txs[tx.txid] = txView;
|
this.txs[tx.txid] = txView;
|
||||||
this.place(txView);
|
this.place(txView);
|
||||||
this.saveGridToScreenPosition(txView);
|
this.saveGridToScreenPosition(txView);
|
||||||
@ -114,7 +122,7 @@ export default class BlockScene {
|
|||||||
});
|
});
|
||||||
txs.forEach(tx => {
|
txs.forEach(tx => {
|
||||||
if (!this.txs[tx.txid]) {
|
if (!this.txs[tx.txid]) {
|
||||||
this.txs[tx.txid] = new TxView(tx, this.vertexArray);
|
this.txs[tx.txid] = new TxView(tx, this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -156,7 +164,7 @@ export default class BlockScene {
|
|||||||
if (resetLayout) {
|
if (resetLayout) {
|
||||||
add.forEach(tx => {
|
add.forEach(tx => {
|
||||||
if (!this.txs[tx.txid]) {
|
if (!this.txs[tx.txid]) {
|
||||||
this.txs[tx.txid] = new TxView(tx, this.vertexArray);
|
this.txs[tx.txid] = new TxView(tx, this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
|
||||||
@ -166,7 +174,7 @@ export default class BlockScene {
|
|||||||
} else {
|
} else {
|
||||||
// try to insert new txs directly
|
// try to insert new txs directly
|
||||||
const remaining = [];
|
const remaining = [];
|
||||||
add.map(tx => new TxView(tx, this.vertexArray)).sort(feeRateDescending).forEach(tx => {
|
add.map(tx => new TxView(tx, this)).sort(feeRateDescending).forEach(tx => {
|
||||||
if (!this.tryInsertByFee(tx)) {
|
if (!this.tryInsertByFee(tx)) {
|
||||||
remaining.push(tx);
|
remaining.push(tx);
|
||||||
}
|
}
|
||||||
@ -192,13 +200,14 @@ export default class BlockScene {
|
|||||||
this.animateUntil = Math.max(this.animateUntil, tx.setHover(value));
|
this.animateUntil = Math.max(this.animateUntil, tx.setHover(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray }:
|
private init({ width, height, resolution, blockLimit, orientation, flip, vertexArray, highlighting }:
|
||||||
{ width: number, height: number, resolution: number, blockLimit: number,
|
{ width: number, height: number, resolution: number, blockLimit: number,
|
||||||
orientation: string, flip: boolean, vertexArray: FastVertexArray }
|
orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean }
|
||||||
): void {
|
): void {
|
||||||
this.orientation = orientation;
|
this.orientation = orientation;
|
||||||
this.flip = flip;
|
this.flip = flip;
|
||||||
this.vertexArray = vertexArray;
|
this.vertexArray = vertexArray;
|
||||||
|
this.highlightingEnabled = highlighting;
|
||||||
|
|
||||||
this.scene = {
|
this.scene = {
|
||||||
count: 0,
|
count: 0,
|
||||||
|
@ -10,12 +10,13 @@ const defaultHoverColor = hexToColor('1bd8f4');
|
|||||||
|
|
||||||
const feeColors = mempoolFeeColors.map(hexToColor);
|
const feeColors = mempoolFeeColors.map(hexToColor);
|
||||||
const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
|
const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9));
|
||||||
|
const marginalFeeColors = feeColors.map((color) => darken(desaturate(color, 0.8), 1.1));
|
||||||
const auditColors = {
|
const auditColors = {
|
||||||
censored: hexToColor('f344df'),
|
censored: hexToColor('f344df'),
|
||||||
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
||||||
added: hexToColor('0099ff'),
|
added: hexToColor('0099ff'),
|
||||||
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
||||||
}
|
};
|
||||||
|
|
||||||
// convert from this class's update format to TxSprite's update format
|
// convert from this class's update format to TxSprite's update format
|
||||||
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
|
function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams {
|
||||||
@ -37,6 +38,7 @@ export default class TxView implements TransactionStripped {
|
|||||||
feerate: number;
|
feerate: number;
|
||||||
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
|
status?: 'found' | 'missing' | 'fresh' | 'added' | 'censored' | 'selected';
|
||||||
context?: 'projected' | 'actual';
|
context?: 'projected' | 'actual';
|
||||||
|
scene?: BlockScene;
|
||||||
|
|
||||||
initialised: boolean;
|
initialised: boolean;
|
||||||
vertexArray: FastVertexArray;
|
vertexArray: FastVertexArray;
|
||||||
@ -49,7 +51,8 @@ export default class TxView implements TransactionStripped {
|
|||||||
|
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
|
|
||||||
constructor(tx: TransactionStripped, vertexArray: FastVertexArray) {
|
constructor(tx: TransactionStripped, scene: BlockScene) {
|
||||||
|
this.scene = scene;
|
||||||
this.context = tx.context;
|
this.context = tx.context;
|
||||||
this.txid = tx.txid;
|
this.txid = tx.txid;
|
||||||
this.fee = tx.fee;
|
this.fee = tx.fee;
|
||||||
@ -58,7 +61,7 @@ export default class TxView implements TransactionStripped {
|
|||||||
this.feerate = tx.fee / tx.vsize;
|
this.feerate = tx.fee / tx.vsize;
|
||||||
this.status = tx.status;
|
this.status = tx.status;
|
||||||
this.initialised = false;
|
this.initialised = false;
|
||||||
this.vertexArray = vertexArray;
|
this.vertexArray = scene.vertexArray;
|
||||||
|
|
||||||
this.hover = false;
|
this.hover = false;
|
||||||
|
|
||||||
@ -156,18 +159,22 @@ export default class TxView implements TransactionStripped {
|
|||||||
getColor(): Color {
|
getColor(): Color {
|
||||||
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1;
|
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, this.feerate) < feeLvl) - 1;
|
||||||
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
|
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
|
||||||
|
// Normal mode
|
||||||
|
if (!this.scene?.highlightingEnabled) {
|
||||||
|
return feeLevelColor;
|
||||||
|
}
|
||||||
// Block audit
|
// Block audit
|
||||||
switch(this.status) {
|
switch(this.status) {
|
||||||
case 'censored':
|
case 'censored':
|
||||||
return auditColors.censored;
|
return auditColors.censored;
|
||||||
case 'missing':
|
case 'missing':
|
||||||
return auditColors.missing;
|
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||||
case 'fresh':
|
case 'fresh':
|
||||||
return auditColors.missing;
|
return auditColors.missing;
|
||||||
case 'added':
|
case 'added':
|
||||||
return auditColors.added;
|
return auditColors.added;
|
||||||
case 'selected':
|
case 'selected':
|
||||||
return auditColors.selected;
|
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||||
case 'found':
|
case 'found':
|
||||||
if (this.context === 'projected') {
|
if (this.context === 'projected') {
|
||||||
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
||||||
|
@ -32,15 +32,15 @@
|
|||||||
<td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
<td class="td-width" i18n="transaction.vsize|Transaction Virtual Size">Virtual size</td>
|
||||||
<td [innerHTML]="'‎' + (vsize | vbytes: 2)"></td>
|
<td [innerHTML]="'‎' + (vsize | vbytes: 2)"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="tx && tx.status && tx.status.length">
|
<tr *ngIf="auditEnabled && tx && tx.status && tx.status.length">
|
||||||
<td class="td-width" i18n="transaction.audit-status">Audit status</td>
|
<td class="td-width" i18n="transaction.audit-status">Audit status</td>
|
||||||
<ng-container [ngSwitch]="tx?.status">
|
<ng-container [ngSwitch]="tx?.status">
|
||||||
<td *ngSwitchCase="'found'" i18n="transaction.audit.match">match</td>
|
<td *ngSwitchCase="'found'"><span class="badge badge-success" i18n="transaction.audit.match">Match</span></td>
|
||||||
<td *ngSwitchCase="'censored'" i18n="transaction.audit.removed">removed</td>
|
<td *ngSwitchCase="'censored'"><span class="badge badge-danger" i18n="transaction.audit.removed">Removed</span></td>
|
||||||
<td *ngSwitchCase="'missing'" i18n="transaction.audit.marginal">marginal fee rate</td>
|
<td *ngSwitchCase="'missing'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||||
<td *ngSwitchCase="'fresh'" i18n="transaction.audit.recently-broadcast">recently broadcast</td>
|
<td *ngSwitchCase="'fresh'"><span class="badge badge-warning" i18n="transaction.audit.recently-broadcasted">Recently broadcasted</span></td>
|
||||||
<td *ngSwitchCase="'added'" i18n="transaction.audit.added">added</td>
|
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
||||||
<td *ngSwitchCase="'selected'" i18n="transaction.audit.marginal">marginal fee rate</td>
|
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -11,6 +11,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
|
|||||||
@Input() tx: TransactionStripped | void;
|
@Input() tx: TransactionStripped | void;
|
||||||
@Input() cursorPosition: Position;
|
@Input() cursorPosition: Position;
|
||||||
@Input() clickable: boolean;
|
@Input() clickable: boolean;
|
||||||
|
@Input() auditEnabled: boolean = false;
|
||||||
|
|
||||||
txid = '';
|
txid = '';
|
||||||
fee = 0;
|
fee = 0;
|
||||||
|
@ -23,7 +23,6 @@
|
|||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
height: calc(100% - 150px);
|
height: calc(100% - 150px);
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
height: 100%;
|
|
||||||
padding-bottom: 100px;
|
padding-bottom: 100px;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@
|
|||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
height: calc(100% - 150px);
|
height: calc(100% - 150px);
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
height: 100%;
|
|
||||||
padding-bottom: 100px;
|
padding-bottom: 100px;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||||
import { EChartsOption, graphic } from 'echarts';
|
import { EChartsOption, graphic } from 'echarts';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { formatCurrency, formatNumber, getCurrencySymbol } from '@angular/common';
|
import { formatNumber } from '@angular/common';
|
||||||
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../shared/graphs.utils';
|
||||||
import { MiningService } from '../../services/mining.service';
|
import { MiningService } from '../../services/mining.service';
|
||||||
|
import { StateService } from '../../services/state.service';
|
||||||
import { StorageService } from '../../services/storage.service';
|
import { StorageService } from '../../services/storage.service';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
||||||
|
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
||||||
|
import { fiatCurrencies } from '../../app.constants';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-block-rewards-graph',
|
selector: 'app-block-rewards-graph',
|
||||||
@ -44,16 +47,28 @@ export class BlockRewardsGraphComponent implements OnInit {
|
|||||||
timespan = '';
|
timespan = '';
|
||||||
chartInstance: any = undefined;
|
chartInstance: any = undefined;
|
||||||
|
|
||||||
|
currencySubscription: Subscription;
|
||||||
|
currency: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private formBuilder: UntypedFormBuilder,
|
private formBuilder: UntypedFormBuilder,
|
||||||
private miningService: MiningService,
|
private miningService: MiningService,
|
||||||
|
private stateService: StateService,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private fiatShortenerPipe: FiatShortenerPipe,
|
private fiatShortenerPipe: FiatShortenerPipe,
|
||||||
|
private fiatCurrencyPipe: FiatCurrencyPipe,
|
||||||
) {
|
) {
|
||||||
|
this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
|
||||||
|
if (fiat && fiatCurrencies[fiat]?.indexed) {
|
||||||
|
this.currency = fiat;
|
||||||
|
} else {
|
||||||
|
this.currency = 'USD';
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -82,7 +97,7 @@ export class BlockRewardsGraphComponent implements OnInit {
|
|||||||
tap((response) => {
|
tap((response) => {
|
||||||
this.prepareChartOptions({
|
this.prepareChartOptions({
|
||||||
blockRewards: response.body.map(val => [val.timestamp * 1000, val.avgRewards / 100000000, val.avgHeight]),
|
blockRewards: response.body.map(val => [val.timestamp * 1000, val.avgRewards / 100000000, val.avgHeight]),
|
||||||
blockRewardsUSD: response.body.filter(val => val.USD > 0).map(val => [val.timestamp * 1000, val.avgRewards / 100000000 * val.USD, val.avgHeight]),
|
blockRewardsFiat: response.body.filter(val => val[this.currency] > 0).map(val => [val.timestamp * 1000, val.avgRewards / 100000000 * val[this.currency], val.avgHeight]),
|
||||||
});
|
});
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}),
|
}),
|
||||||
@ -157,7 +172,7 @@ export class BlockRewardsGraphComponent implements OnInit {
|
|||||||
if (tick.seriesIndex === 0) {
|
if (tick.seriesIndex === 0) {
|
||||||
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.3-3')} BTC<br>`;
|
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.3-3')} BTC<br>`;
|
||||||
} else if (tick.seriesIndex === 1) {
|
} else if (tick.seriesIndex === 1) {
|
||||||
tooltip += `${tick.marker} ${tick.seriesName}: ${formatCurrency(tick.data[1], this.locale, getCurrencySymbol('USD', 'narrow'), 'USD', '1.0-0')}<br>`;
|
tooltip += `${tick.marker} ${tick.seriesName}: ${this.fiatCurrencyPipe.transform(tick.data[1], null, this.currency)}<br>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +199,7 @@ export class BlockRewardsGraphComponent implements OnInit {
|
|||||||
icon: 'roundRect',
|
icon: 'roundRect',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Rewards USD',
|
name: 'Rewards ' + this.currency,
|
||||||
inactiveColor: 'rgb(110, 112, 121)',
|
inactiveColor: 'rgb(110, 112, 121)',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: 'white',
|
color: 'white',
|
||||||
@ -228,7 +243,7 @@ export class BlockRewardsGraphComponent implements OnInit {
|
|||||||
axisLabel: {
|
axisLabel: {
|
||||||
color: 'rgb(110, 112, 121)',
|
color: 'rgb(110, 112, 121)',
|
||||||
formatter: function(val) {
|
formatter: function(val) {
|
||||||
return this.fiatShortenerPipe.transform(val);
|
return this.fiatShortenerPipe.transform(val, null, this.currency);
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
@ -251,8 +266,8 @@ export class BlockRewardsGraphComponent implements OnInit {
|
|||||||
legendHoverLink: false,
|
legendHoverLink: false,
|
||||||
zlevel: 1,
|
zlevel: 1,
|
||||||
yAxisIndex: 1,
|
yAxisIndex: 1,
|
||||||
name: 'Rewards USD',
|
name: 'Rewards ' + this.currency,
|
||||||
data: data.blockRewardsUSD,
|
data: data.blockRewardsFiat,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
smooth: 0.25,
|
smooth: 0.25,
|
||||||
symbol: 'none',
|
symbol: 'none',
|
||||||
|
@ -23,7 +23,6 @@
|
|||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
height: calc(100% - 150px);
|
height: calc(100% - 150px);
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
height: 100%;
|
|
||||||
padding-bottom: 100px;
|
padding-bottom: 100px;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -53,13 +53,13 @@
|
|||||||
<td i18n="block.miner">Miner</td>
|
<td i18n="block.miner">Miner</td>
|
||||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||||
<a [attr.data-cy]="'block-details-miner-badge'" placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block?.extras.pool.slug]" class="badge"
|
<a [attr.data-cy]="'block-details-miner-badge'" placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block?.extras.pool.slug]" class="badge"
|
||||||
[class]="block?.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
[class]="!block?.extras.pool.name || block?.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||||
{{ block?.extras.pool.name }}
|
{{ block?.extras.pool.name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
||||||
<span [attr.data-cy]="'block-details-miner-badge'" placement="bottom" class="badge"
|
<span [attr.data-cy]="'block-details-miner-badge'" placement="bottom" class="badge"
|
||||||
[class]="block?.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
[class]="!block?.extras.pool.name || block?.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||||
{{ block?.extras.pool.name }}
|
{{ block?.extras.pool.name }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -56,3 +56,7 @@
|
|||||||
::ng-deep .symbol {
|
::ng-deep .symbol {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
@ -28,14 +28,12 @@
|
|||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="box" *ngIf="!error">
|
<div class="box" *ngIf="!error">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<ng-template [ngIf]="!isLoadingBlock">
|
<div class="col-sm">
|
||||||
<div class="col-sm">
|
<table class="table table-borderless table-striped">
|
||||||
<table class="table table-borderless table-striped">
|
<tbody>
|
||||||
<tbody>
|
<ng-container *ngIf="!isLoadingBlock; else skeletonRows">
|
||||||
<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>‎<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>
|
||||||
@ -54,83 +52,28 @@
|
|||||||
<td i18n="block.weight">Weight</td>
|
<td i18n="block.weight">Weight</td>
|
||||||
<td [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></td>
|
<td [innerHTML]="'‎' + (block.weight | wuBytes: 2)"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="auditEnabled">
|
<tr *ngIf="auditAvailable">
|
||||||
<td i18n="block.health">Block health</td>
|
<td><ng-container i18n="latest-blocks.health">Health</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="what-is-block-health"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></td>
|
||||||
<td>
|
<td>
|
||||||
<span *ngIf="blockAudit?.matchRate != null">{{ blockAudit.matchRate }}%</span>
|
<span
|
||||||
<span *ngIf="blockAudit?.matchRate === null" i18n="unknown">Unknown</span>
|
class="health-badge badge"
|
||||||
|
[class.badge-success]="blockAudit?.matchRate >= 99"
|
||||||
|
[class.badge-warning]="blockAudit?.matchRate >= 75 && blockAudit?.matchRate < 99"
|
||||||
|
[class.badge-danger]="blockAudit?.matchRate < 75"
|
||||||
|
*ngIf="blockAudit?.matchRate != null; else nullHealth"
|
||||||
|
>{{ blockAudit?.matchRate }}%</span>
|
||||||
|
<ng-template #nullHealth>
|
||||||
|
<ng-container *ngIf="!isLoadingAudit; else loadingHealth">
|
||||||
|
<span class="health-badge badge badge-secondary" i18n="unknown">Unknown</span>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #loadingHealth>
|
||||||
|
<span class="skeleton-loader" style="max-width: 60px"></span>
|
||||||
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<ng-container *ngIf="webGlEnabled && (auditDataMissing || !indexingAvailable)">
|
</ng-container>
|
||||||
<tr *ngIf="isMobile && auditEnabled"></tr>
|
<ng-template #skeletonRows>
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
|
||||||
<td i18n="mempool-block.fee-span">Fee span</td>
|
|
||||||
<td><span>{{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr *ngIf="block?.extras?.medianFee != undefined">
|
|
||||||
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
|
||||||
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
|
||||||
</tr>
|
|
||||||
<ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees">
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
|
||||||
<td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees">
|
|
||||||
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
|
||||||
<span class="fiat">
|
|
||||||
<app-fiat [value]="block.extras.totalFees" digitsInfo="1.0-0"></app-fiat>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<ng-template #liquidTotalFees>
|
|
||||||
<td>
|
|
||||||
<app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <app-fiat
|
|
||||||
[value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat>
|
|
||||||
</td>
|
|
||||||
</ng-template>
|
|
||||||
</tr>
|
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
|
||||||
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
|
|
||||||
<td>
|
|
||||||
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
|
||||||
<span class="fiat">
|
|
||||||
<app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
<ng-template #loadingFees>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
|
||||||
<td style="width: 75%;"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
|
||||||
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
|
|
||||||
<td><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
|
||||||
<td i18n="block.miner">Miner</td>
|
|
||||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
|
||||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
|
|
||||||
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
|
||||||
{{ block.extras.pool.name }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
|
||||||
<span placement="bottom" class="badge"
|
|
||||||
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
|
||||||
{{ block.extras.pool.name }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</ng-container>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
<ng-template [ngIf]="isLoadingBlock">
|
|
||||||
<div class="col-sm">
|
|
||||||
<table class="table table-borderless table-striped">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -143,117 +86,21 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
<tr *ngIf="showAudit">
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<ng-container *ngIf="webGlEnabled && (!indexingAvailable || auditDataMissing)">
|
</ng-template>
|
||||||
<tr *ngIf="isMobile && !auditEnabled"></tr>
|
<ng-container *ngIf="isMobile || (webGlEnabled && !showAudit); then restOfTable;"></ng-container>
|
||||||
<tr>
|
</tbody>
|
||||||
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
</table>
|
||||||
</tr>
|
</div>
|
||||||
<tr>
|
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
</ng-container>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<table class="table table-borderless table-striped" *ngIf="!isLoadingBlock && (!auditDataMissing || indexingAvailable && !webGlEnabled)">
|
<table class="table table-borderless table-striped" *ngIf="!isMobile && !(webGlEnabled && !showAudit)">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngIf="isMobile && auditEnabled"></tr>
|
<ng-container *ngTemplateOutlet="restOfTable"></ng-container>
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
|
||||||
<td i18n="mempool-block.fee-span">Fee span</td>
|
|
||||||
<td><span>{{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr *ngIf="block?.extras?.medianFee != undefined">
|
|
||||||
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
|
||||||
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
|
||||||
</tr>
|
|
||||||
<ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees">
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
|
||||||
<td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees">
|
|
||||||
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
|
||||||
<span class="fiat">
|
|
||||||
<app-fiat [value]="block.extras.totalFees" digitsInfo="1.0-0"></app-fiat>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<ng-template #liquidTotalFees>
|
|
||||||
<td>
|
|
||||||
<app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <app-fiat
|
|
||||||
[value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat>
|
|
||||||
</td>
|
|
||||||
</ng-template>
|
|
||||||
</tr>
|
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
|
||||||
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
|
|
||||||
<td>
|
|
||||||
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
|
||||||
<span class="fiat">
|
|
||||||
<app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
<ng-template #loadingFees>
|
|
||||||
<tr>
|
|
||||||
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
|
||||||
<td style="width: 75%;"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
|
||||||
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees:</td>
|
|
||||||
<td><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
</ng-template>
|
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
|
||||||
<td i18n="block.miner">Miner</td>
|
|
||||||
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
|
||||||
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
|
|
||||||
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
|
||||||
{{ block.extras.pool.name }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
|
||||||
<span placement="bottom" class="badge"
|
|
||||||
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
|
||||||
{{ block.extras.pool.name }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<table class="table table-borderless table-striped" *ngIf="isLoadingBlock && !auditDataMissing && (indexingAvailable || !webGlEnabled)">
|
<div class="col-sm chart-container" *ngIf="webGlEnabled && !showAudit">
|
||||||
<tbody>
|
|
||||||
<tr *ngIf="isMobile && !auditEnabled"></tr>
|
|
||||||
<tr>
|
|
||||||
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
|
||||||
<td colspan="2"><span class="skeleton-loader"></span></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div class="col-sm chart-container" *ngIf="webGlEnabled && (!indexingAvailable || auditDataMissing)">
|
|
||||||
<app-block-overview-graph
|
<app-block-overview-graph
|
||||||
#blockGraphActual
|
#blockGraphActual
|
||||||
[isLoading]="isLoadingOverview"
|
[isLoading]="isLoadingOverview"
|
||||||
@ -263,39 +110,128 @@
|
|||||||
[flip]="false"
|
[flip]="false"
|
||||||
(txClickEvent)="onTxClick($event)"
|
(txClickEvent)="onTxClick($event)"
|
||||||
></app-block-overview-graph>
|
></app-block-overview-graph>
|
||||||
|
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span id="overview"></span>
|
<ng-template #restOfTable>
|
||||||
|
<ng-container *ngIf="!isLoadingBlock; else loadingRest">
|
||||||
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td i18n="mempool-block.fee-span">Fee span</td>
|
||||||
|
<td><span>{{ block.extras.feeRange[0] | number:'1.0-0' }} - {{ block.extras.feeRange[block.extras.feeRange.length - 1] | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="block?.extras?.medianFee != undefined">
|
||||||
|
<td class="td-width" i18n="block.median-fee">Median fee</td>
|
||||||
|
<td>~{{ block?.extras?.medianFee | number:'1.0-0' }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="fiat"><app-fiat [value]="block?.extras?.medianFee * 140" digitsInfo="1.2-2" i18n-ngbTooltip="Transaction fee tooltip" ngbTooltip="Based on average native segwit transaction of 140 vBytes" placement="bottom"></app-fiat></span></td>
|
||||||
|
</tr>
|
||||||
|
<ng-template [ngIf]="fees !== undefined" [ngIfElse]="loadingFees">
|
||||||
|
<tr>
|
||||||
|
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
||||||
|
<td *ngIf="network !== 'liquid' && network !== 'liquidtestnet'; else liquidTotalFees">
|
||||||
|
<app-amount [satoshis]="block.extras.totalFees" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||||
|
<span class="fiat">
|
||||||
|
<app-fiat [value]="block.extras.totalFees" digitsInfo="1.0-0"></app-fiat>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<ng-template #liquidTotalFees>
|
||||||
|
<td>
|
||||||
|
<app-amount [satoshis]="fees * 100000000" digitsInfo="1.2-2" [noFiat]="true"></app-amount> <app-fiat
|
||||||
|
[value]="fees * 100000000" digitsInfo="1.2-2"></app-fiat>
|
||||||
|
</td>
|
||||||
|
</ng-template>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees</td>
|
||||||
|
<td>
|
||||||
|
<app-amount [satoshis]="block.extras.reward" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||||
|
<span class="fiat">
|
||||||
|
<app-fiat [value]="(blockSubsidy + fees) * 100000000" digitsInfo="1.0-0"></app-fiat>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #loadingFees>
|
||||||
|
<tr>
|
||||||
|
<td i18n="block.total-fees|Total fees in a block">Total fees</td>
|
||||||
|
<td style="width: 75%;"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td i18n="block.subsidy-and-fees|Total subsidy and fees in a block">Subsidy + fees</td>
|
||||||
|
<td><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td i18n="block.miner">Miner</td>
|
||||||
|
<td *ngIf="stateService.env.MINING_DASHBOARD">
|
||||||
|
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge"
|
||||||
|
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||||
|
{{ block.extras.pool.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!stateService.env.MINING_DASHBOARD && stateService.env.BASE_MODULE === 'mempool'">
|
||||||
|
<span placement="bottom" class="badge"
|
||||||
|
[class]="block.extras.pool.name === 'Unknown' ? 'badge-secondary' : 'badge-primary'">
|
||||||
|
{{ block.extras.pool.name }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #loadingRest>
|
||||||
|
<tr>
|
||||||
|
<td class="td-width" colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="network !== 'liquid' && network !== 'liquidtestnet'">
|
||||||
|
<td colspan="2"><span class="skeleton-loader"></span></td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<br>
|
<ng-container *ngIf="showAudit">
|
||||||
|
<span id="overview"></span>
|
||||||
|
<br>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<!-- VISUALIZATIONS -->
|
<!-- VISUALIZATIONS -->
|
||||||
<div class="box" *ngIf="!error && webGlEnabled && indexingAvailable && !auditDataMissing">
|
<div class="box" *ngIf="!error && webGlEnabled && showAudit">
|
||||||
<div class="nav nav-tabs" *ngIf="isMobile && auditEnabled">
|
<div class="nav nav-tabs" *ngIf="isMobile && showAudit">
|
||||||
<a class="nav-link" [class.active]="mode === 'projected'" i18n="block.projected"
|
<a class="nav-link" [class.active]="mode === 'projected'"
|
||||||
fragment="projected" (click)="changeMode('projected')">Projected</a>
|
fragment="projected" (click)="changeMode('projected')"><ng-container i18n="block.expected">Expected</ng-container> <span class="badge badge-pill badge-warning" i18n="beta">beta</span></a>
|
||||||
<a class="nav-link" [class.active]="mode === 'actual'" i18n="block.actual"
|
<a class="nav-link" [class.active]="mode === 'actual'" i18n="block.actual"
|
||||||
fragment="actual" (click)="changeMode('actual')">Actual</a>
|
fragment="actual" (click)="changeMode('actual')">Actual</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.projected-block">Projected Block</h3>
|
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container> <span class="badge badge-pill badge-warning beta" i18n="beta">beta</span></h3>
|
||||||
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="75"
|
<div class="block-graph-wrapper">
|
||||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx"
|
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="75"
|
||||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !auditEnabled"></app-block-overview-graph>
|
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
|
||||||
|
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"></app-block-overview-graph>
|
||||||
|
<ng-container *ngIf="!isMobile || mode !== 'actual'; else emptyBlockInfo"></ng-container>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm" *ngIf="!isMobile">
|
<div class="col-sm" *ngIf="!isMobile">
|
||||||
<h3 class="block-subtitle" *ngIf="!isMobile" i18n="block.actual-block">Actual Block</h3>
|
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container> <a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
|
||||||
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="75"
|
<div class="block-graph-wrapper">
|
||||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined"
|
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="75"
|
||||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !auditEnabled"></app-block-overview-graph>
|
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"
|
||||||
|
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"></app-block-overview-graph>
|
||||||
|
<ng-container *ngTemplateOutlet="emptyBlockInfo"></ng-container>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template [ngIf]="!isLoadingBlock && !error">
|
<ng-template [ngIf]="!isLoadingBlock && !error">
|
||||||
<div [hidden]="!showDetails" id="details">
|
<div [hidden]="!showDetails" id="details">
|
||||||
<br>
|
<br>
|
||||||
@ -343,8 +279,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-right mt-3">
|
<div class="text-right mt-3 toggle-btns">
|
||||||
<button type="button" class="btn btn-outline-info btn-sm btn-details" (click)="toggleShowDetails()" i18n="transaction.details|Transaction Details">Details</button>
|
<button
|
||||||
|
*ngIf="webGlEnabled && auditAvailable"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-info btn-sm btn-audit"
|
||||||
|
[class.active]="auditModeEnabled"
|
||||||
|
(click)="toggleAuditMode()"
|
||||||
|
i18n="block.toggle-audit|Toggle Audit"
|
||||||
|
>Audit</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-info btn-sm btn-details"
|
||||||
|
[class.active]="showDetails"
|
||||||
|
(click)="toggleShowDetails()"
|
||||||
|
i18n="transaction.details|Transaction Details"
|
||||||
|
>Details</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div #blockTxTitle id="block-tx-title" class="block-tx-title">
|
<div #blockTxTitle id="block-tx-title" class="block-tx-title">
|
||||||
@ -396,6 +346,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ngb-pagination class="pagination-container float-right" [collectionSize]="block.tx_count" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page, blockTxTitle)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
|
<ngb-pagination class="pagination-container float-right" [collectionSize]="block.tx_count" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page, blockTxTitle)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
|
||||||
|
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
<br>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template [ngIf]="error">
|
<ng-template [ngIf]="error">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
@ -413,5 +366,17 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-template #emptyBlockInfo>
|
||||||
|
<a
|
||||||
|
*ngIf="network === '' && block && block.height > 100000 && block.tx_count <= 1"
|
||||||
|
class="info-bubble-link badge badge-primary"
|
||||||
|
[routerLink]="['/docs/faq/' | relativeUrl]"
|
||||||
|
fragment="why-empty-blocks"
|
||||||
|
>
|
||||||
|
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
|
||||||
|
<span i18n="block.empty-block-explanation">Why is this block empty?</span>
|
||||||
|
</a>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
|
@ -34,9 +34,24 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.info-link {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block-subtitle.actual a {
|
||||||
|
position: absolute;
|
||||||
|
top: -3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-subtitle.actual fa-icon {
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
font-size: 18px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
@ -75,14 +90,19 @@ h1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-details {
|
.toggle-btns {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
top: 7px;
|
top: 7px;
|
||||||
@media (min-width: 550px) {
|
@media (min-width: 550px) {
|
||||||
top: 0px;
|
top: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-audit {
|
||||||
|
margin-right: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
.block-tx-title {
|
.block-tx-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -202,4 +222,33 @@ h1 {
|
|||||||
&.active, &:hover {
|
&.active, &:hover {
|
||||||
border-color: white;
|
border-color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block-graph-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-bubble-link {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
top: 2em;
|
||||||
|
left: 50%;
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
font-size: 80%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
|
||||||
|
.ng-fa-icon {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.beta {
|
||||||
|
font-size: 10px;
|
||||||
|
margin: 5p;
|
||||||
|
padding: 5p;
|
||||||
|
position: absolute;
|
||||||
|
top: 11px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
@ -43,6 +43,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
strippedTransactions: TransactionStripped[];
|
strippedTransactions: TransactionStripped[];
|
||||||
overviewTransitionDirection: string;
|
overviewTransitionDirection: string;
|
||||||
isLoadingOverview = true;
|
isLoadingOverview = true;
|
||||||
|
isLoadingAudit = true;
|
||||||
error: any;
|
error: any;
|
||||||
blockSubsidy: number;
|
blockSubsidy: number;
|
||||||
fees: number;
|
fees: number;
|
||||||
@ -56,9 +57,10 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
transactionsError: any = null;
|
transactionsError: any = null;
|
||||||
overviewError: any = null;
|
overviewError: any = null;
|
||||||
webGlEnabled = true;
|
webGlEnabled = true;
|
||||||
indexingAvailable = false;
|
auditSupported: boolean = this.stateService.env.AUDIT && this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true;
|
||||||
auditEnabled = true;
|
auditModeEnabled: boolean = !this.stateService.hideAudit.value;
|
||||||
auditDataMissing: boolean;
|
auditAvailable = true;
|
||||||
|
showAudit: boolean;
|
||||||
isMobile = window.innerWidth <= 767.98;
|
isMobile = window.innerWidth <= 767.98;
|
||||||
hoverTx: string;
|
hoverTx: string;
|
||||||
numMissing: number = 0;
|
numMissing: number = 0;
|
||||||
@ -78,6 +80,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
timeLtrSubscription: Subscription;
|
timeLtrSubscription: Subscription;
|
||||||
timeLtr: boolean;
|
timeLtr: boolean;
|
||||||
childChangeSubscription: Subscription;
|
childChangeSubscription: Subscription;
|
||||||
|
auditPrefSubscription: Subscription;
|
||||||
|
|
||||||
@ViewChildren('blockGraphProjected') blockGraphProjected: QueryList<BlockOverviewGraphComponent>;
|
@ViewChildren('blockGraphProjected') blockGraphProjected: QueryList<BlockOverviewGraphComponent>;
|
||||||
@ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
|
@ViewChildren('blockGraphActual') blockGraphActual: QueryList<BlockOverviewGraphComponent>;
|
||||||
@ -106,8 +109,14 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.timeLtr = !!ltr;
|
this.timeLtr = !!ltr;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.indexingAvailable = (this.stateService.env.BASE_MODULE === 'mempool' && this.stateService.env.MINING_DASHBOARD === true);
|
this.setAuditAvailable(this.auditSupported);
|
||||||
this.auditEnabled = this.indexingAvailable;
|
|
||||||
|
if (this.auditSupported) {
|
||||||
|
this.auditPrefSubscription = this.stateService.hideAudit.subscribe((hide) => {
|
||||||
|
this.auditModeEnabled = !hide;
|
||||||
|
this.showAudit = this.auditAvailable && this.auditModeEnabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.txsLoadingStatus$ = this.route.paramMap
|
this.txsLoadingStatus$ = this.route.paramMap
|
||||||
.pipe(
|
.pipe(
|
||||||
@ -138,11 +147,10 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.error = undefined;
|
this.error = undefined;
|
||||||
this.fees = undefined;
|
this.fees = undefined;
|
||||||
this.stateService.markBlock$.next({});
|
this.stateService.markBlock$.next({});
|
||||||
this.auditDataMissing = false;
|
|
||||||
|
|
||||||
if (history.state.data && history.state.data.blockHeight) {
|
if (history.state.data && history.state.data.blockHeight) {
|
||||||
this.blockHeight = history.state.data.blockHeight;
|
this.blockHeight = history.state.data.blockHeight;
|
||||||
this.updateAuditDataMissingFromBlockHeight(this.blockHeight);
|
this.updateAuditAvailableFromBlockHeight(this.blockHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
let isBlockHeight = false;
|
let isBlockHeight = false;
|
||||||
@ -155,7 +163,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
if (history.state.data && history.state.data.block) {
|
if (history.state.data && history.state.data.block) {
|
||||||
this.blockHeight = history.state.data.block.height;
|
this.blockHeight = history.state.data.block.height;
|
||||||
this.updateAuditDataMissingFromBlockHeight(this.blockHeight);
|
this.updateAuditAvailableFromBlockHeight(this.blockHeight);
|
||||||
return of(history.state.data.block);
|
return of(history.state.data.block);
|
||||||
} else {
|
} else {
|
||||||
this.isLoadingBlock = true;
|
this.isLoadingBlock = true;
|
||||||
@ -214,10 +222,12 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe();
|
this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe();
|
||||||
this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe();
|
this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe();
|
||||||
this.apiService.getBlockAudit$(block.previousblockhash);
|
if (this.auditSupported) {
|
||||||
|
this.apiService.getBlockAudit$(block.previousblockhash);
|
||||||
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
this.updateAuditDataMissingFromBlockHeight(block.height);
|
this.updateAuditAvailableFromBlockHeight(block.height);
|
||||||
this.block = block;
|
this.block = block;
|
||||||
this.blockHeight = block.height;
|
this.blockHeight = block.height;
|
||||||
this.lastBlockHeight = this.blockHeight;
|
this.lastBlockHeight = this.blockHeight;
|
||||||
@ -262,7 +272,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
this.isLoadingOverview = false;
|
this.isLoadingOverview = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.indexingAvailable) {
|
if (!this.auditSupported) {
|
||||||
this.overviewSubscription = block$.pipe(
|
this.overviewSubscription = block$.pipe(
|
||||||
startWith(null),
|
startWith(null),
|
||||||
pairwise(),
|
pairwise(),
|
||||||
@ -293,17 +303,22 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.indexingAvailable) {
|
if (this.auditSupported) {
|
||||||
this.auditSubscription = block$.pipe(
|
this.auditSubscription = block$.pipe(
|
||||||
startWith(null),
|
startWith(null),
|
||||||
pairwise(),
|
pairwise(),
|
||||||
switchMap(([prevBlock, block]) => this.apiService.getBlockAudit$(block.id)
|
switchMap(([prevBlock, block]) => {
|
||||||
.pipe(
|
this.isLoadingAudit = true;
|
||||||
catchError((err) => {
|
this.blockAudit = null;
|
||||||
this.overviewError = err;
|
return this.apiService.getBlockAudit$(block.id)
|
||||||
return of([]);
|
.pipe(
|
||||||
})
|
catchError((err) => {
|
||||||
)
|
this.overviewError = err;
|
||||||
|
this.isLoadingAudit = false;
|
||||||
|
return of([]);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
),
|
),
|
||||||
filter((response) => response != null),
|
filter((response) => response != null),
|
||||||
map((response) => {
|
map((response) => {
|
||||||
@ -364,10 +379,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
for (const tx of blockAudit.transactions) {
|
for (const tx of blockAudit.transactions) {
|
||||||
inBlock[tx.txid] = true;
|
inBlock[tx.txid] = true;
|
||||||
}
|
}
|
||||||
this.auditEnabled = true;
|
this.setAuditAvailable(true);
|
||||||
} else {
|
} else {
|
||||||
this.auditEnabled = false;
|
this.setAuditAvailable(false);
|
||||||
this.auditDataMissing = true;
|
|
||||||
}
|
}
|
||||||
return blockAudit;
|
return blockAudit;
|
||||||
}),
|
}),
|
||||||
@ -375,12 +389,15 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
this.error = err;
|
this.error = err;
|
||||||
this.isLoadingOverview = false;
|
this.isLoadingOverview = false;
|
||||||
|
this.isLoadingAudit = false;
|
||||||
|
this.setAuditAvailable(false);
|
||||||
return of(null);
|
return of(null);
|
||||||
}),
|
}),
|
||||||
).subscribe((blockAudit) => {
|
).subscribe((blockAudit) => {
|
||||||
this.blockAudit = blockAudit;
|
this.blockAudit = blockAudit;
|
||||||
this.setupBlockGraphs();
|
this.setupBlockGraphs();
|
||||||
this.isLoadingOverview = false;
|
this.isLoadingOverview = false;
|
||||||
|
this.isLoadingAudit = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,16 +442,17 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.stateService.markBlock$.next({});
|
this.stateService.markBlock$.next({});
|
||||||
this.transactionSubscription.unsubscribe();
|
this.transactionSubscription?.unsubscribe();
|
||||||
this.overviewSubscription?.unsubscribe();
|
this.overviewSubscription?.unsubscribe();
|
||||||
this.auditSubscription?.unsubscribe();
|
this.auditSubscription?.unsubscribe();
|
||||||
this.keyNavigationSubscription.unsubscribe();
|
this.keyNavigationSubscription?.unsubscribe();
|
||||||
this.blocksSubscription.unsubscribe();
|
this.blocksSubscription?.unsubscribe();
|
||||||
this.networkChangedSubscription.unsubscribe();
|
this.networkChangedSubscription?.unsubscribe();
|
||||||
this.queryParamsSubscription.unsubscribe();
|
this.queryParamsSubscription?.unsubscribe();
|
||||||
this.timeLtrSubscription.unsubscribe();
|
this.timeLtrSubscription?.unsubscribe();
|
||||||
|
this.auditSubscription?.unsubscribe();
|
||||||
this.unsubscribeNextBlockSubscriptions();
|
this.unsubscribeNextBlockSubscriptions();
|
||||||
this.childChangeSubscription.unsubscribe();
|
this.childChangeSubscription?.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribeNextBlockSubscriptions() {
|
unsubscribeNextBlockSubscriptions() {
|
||||||
@ -588,21 +606,33 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAuditDataMissingFromBlockHeight(blockHeight: number): void {
|
setAuditAvailable(available: boolean): void {
|
||||||
|
this.auditAvailable = available;
|
||||||
|
this.showAudit = this.auditAvailable && this.auditModeEnabled && this.auditSupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleAuditMode(): void {
|
||||||
|
this.stateService.hideAudit.next(this.auditModeEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAuditAvailableFromBlockHeight(blockHeight: number): void {
|
||||||
|
if (!this.auditSupported) {
|
||||||
|
this.setAuditAvailable(false);
|
||||||
|
}
|
||||||
switch (this.stateService.network) {
|
switch (this.stateService.network) {
|
||||||
case 'testnet':
|
case 'testnet':
|
||||||
if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) {
|
if (blockHeight < this.stateService.env.TESTNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||||
this.auditDataMissing = true;
|
this.setAuditAvailable(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'signet':
|
case 'signet':
|
||||||
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
|
if (blockHeight < this.stateService.env.SIGNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||||
this.auditDataMissing = true;
|
this.setAuditAvailable(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) {
|
if (blockHeight < this.stateService.env.MAINNET_BLOCK_AUDIT_START_HEIGHT) {
|
||||||
this.auditDataMissing = true;
|
this.setAuditAvailable(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,55 @@
|
|||||||
<div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr" *ngIf="(loadingBlocks$ | async) === false; else loadingBlocksTemplate">
|
<div class="blocks-container blockchain-blocks-container" [class.time-ltr]="timeLtr" [style.left]="static ? (offset || 0) + 'px' : null" *ngIf="(loadingBlocks$ | async) === false; else loadingBlocksTemplate">
|
||||||
<div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn" >
|
<div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn">
|
||||||
<div [attr.data-cy]="'bitcoin-block-' + i" class="text-center bitcoin-block mined-block blockchain-blocks-{{ i }}" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]" [class.blink-bg]="(specialBlocks[block.height] !== undefined)">
|
<ng-container *ngIf="block && !block.loading && !block.placeholder; else placeholderBlock">
|
||||||
<a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
|
<div [attr.data-cy]="'bitcoin-block-' + i" class="text-center bitcoin-block mined-block blockchain-blocks-{{ i }}" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]" [class.blink-bg]="(specialBlocks[block.height] !== undefined)">
|
||||||
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
<a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
|
||||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
|
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}"> </a>
|
||||||
<a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a>
|
<div [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
|
||||||
|
<a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="block-body">
|
||||||
|
<div [attr.data-cy]="'bitcoin-block-' + i + '-fees'" class="fees">
|
||||||
|
~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||||
|
</div>
|
||||||
|
<div [attr.data-cy]="'bitcoin-block-' + i + '-fee-span'" class="fee-span" *ngIf="block?.extras?.feeRange">
|
||||||
|
{{ block?.extras?.feeRange?.[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange?.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
||||||
|
</div>
|
||||||
|
<div [attr.data-cy]="'bitcoin-block-' + i + '-fee-span'" class="fee-span" *ngIf="!block?.extras?.feeRange">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div [attr.data-cy]="'bitcoin-block-' + i + '-total-fees'" *ngIf="showMiningInfo" class="block-size">
|
||||||
|
<app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
||||||
|
</div>
|
||||||
|
<div [attr.data-cy]="'bitcoin-block-' + i + 'block-size'" *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div>
|
||||||
|
<div [attr.data-cy]="'bitcoin-block-' + i + '-transactions'" class="transaction-count">
|
||||||
|
<ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
|
||||||
|
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
||||||
|
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
||||||
|
</div>
|
||||||
|
<div [attr.data-cy]="'bitcoin-block-' + i + '-time'" class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div>
|
||||||
|
</div>
|
||||||
|
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
|
||||||
|
<a [attr.data-cy]="'bitcoin-block-' + i + '-pool'" class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
||||||
|
{{ block.extras.pool.name}}</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="block-body">
|
</ng-container>
|
||||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-fees'" class="fees">
|
<ng-template #placeholderBlock>
|
||||||
~{{ block?.extras?.medianFee | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
<ng-container *ngIf="block && block.placeholder; else loadingBlock">
|
||||||
|
<div [attr.data-cy]="'bitcoin-block-' + i" class="text-center bitcoin-block mined-block placeholder-block blockchain-blocks-{{ i }}" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-fee-span'" class="fee-span">
|
</ng-container>
|
||||||
{{ block?.extras?.feeRange[1] | number:feeRounding }} - {{ block?.extras?.feeRange[block?.extras?.feeRange.length - 1] | number:feeRounding }} <ng-container i18n="shared.sat-vbyte|sat/vB">sat/vB</ng-container>
|
</ng-template>
|
||||||
|
<ng-template #loadingBlock>
|
||||||
|
<ng-container *ngIf="block && block.loading">
|
||||||
|
<div class="flashing">
|
||||||
|
<div class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"></div>
|
||||||
</div>
|
</div>
|
||||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-total-fees'" *ngIf="showMiningInfo" class="block-size">
|
</ng-container>
|
||||||
<app-amount [satoshis]="block.extras?.totalFees ?? 0" digitsInfo="1.2-3" [noFiat]="true"></app-amount>
|
</ng-template>
|
||||||
</div>
|
|
||||||
<div [attr.data-cy]="'bitcoin-block-' + i + 'block-size'" *ngIf="!showMiningInfo" class="block-size" [innerHTML]="'‎' + (block.size | bytes: 2)"></div>
|
|
||||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-transactions'" class="transaction-count">
|
|
||||||
<ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
|
|
||||||
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
|
|
||||||
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
|
|
||||||
</div>
|
|
||||||
<div [attr.data-cy]="'bitcoin-block-' + i + '-time'" class="time-difference"><app-time-since [time]="block.timestamp" [fastRender]="true"></app-time-since></div>
|
|
||||||
</div>
|
|
||||||
<div class="animated" [class]="showMiningInfo ? 'show' : 'hide'" *ngIf="block.extras?.pool != undefined">
|
|
||||||
<a [attr.data-cy]="'bitcoin-block-' + i + '-pool'" class="badge badge-primary" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]">
|
|
||||||
{{ block.extras.pool.name}}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="transition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div>
|
<div [hidden]="!arrowVisible" id="arrow-up" [style.transition]="arrowTransition" [ngStyle]="{'left': arrowLeftPx + 'px' }"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-template #loadingBlocksTemplate>
|
<ng-template #loadingBlocksTemplate>
|
||||||
|
@ -25,6 +25,10 @@
|
|||||||
transition: background 2s, left 2s, transform 1s;
|
transition: background 2s, left 2s, transform 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mined-block.placeholder-block {
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.block-size {
|
.block-size {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -96,6 +100,16 @@
|
|||||||
transform-origin: top;
|
transform-origin: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bitcoin-block.placeholder-block::after {
|
||||||
|
content: none;
|
||||||
|
background: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bitcoin-block.placeholder-block::before {
|
||||||
|
content: none;
|
||||||
|
background: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.black-background {
|
.black-background {
|
||||||
background-color: #11131f;
|
background-color: #11131f;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
import { Observable, Subscription } from 'rxjs';
|
import { Observable, Subscription } from 'rxjs';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { specialBlocks } from '../../app.constants';
|
import { specialBlocks } from '../../app.constants';
|
||||||
import { BlockExtended } from '../../interfaces/node-api.interface';
|
import { BlockExtended } from '../../interfaces/node-api.interface';
|
||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { config } from 'process';
|
import { CacheService } from '../../services/cache.service';
|
||||||
|
|
||||||
|
interface BlockchainBlock extends BlockExtended {
|
||||||
|
placeholder?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-blockchain-blocks',
|
selector: 'app-blockchain-blocks',
|
||||||
@ -12,13 +17,20 @@ import { config } from 'process';
|
|||||||
styleUrls: ['./blockchain-blocks.component.scss'],
|
styleUrls: ['./blockchain-blocks.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
|
@Input() static: boolean = false;
|
||||||
|
@Input() offset: number = 0;
|
||||||
|
@Input() height: number = 0;
|
||||||
|
@Input() count: number = 8;
|
||||||
|
|
||||||
specialBlocks = specialBlocks;
|
specialBlocks = specialBlocks;
|
||||||
network = '';
|
network = '';
|
||||||
blocks: BlockExtended[] = [];
|
blocks: BlockchainBlock[] = [];
|
||||||
|
dynamicBlocksAmount: number = 8;
|
||||||
emptyBlocks: BlockExtended[] = this.mountEmptyBlocks();
|
emptyBlocks: BlockExtended[] = this.mountEmptyBlocks();
|
||||||
markHeight: number;
|
markHeight: number;
|
||||||
blocksSubscription: Subscription;
|
blocksSubscription: Subscription;
|
||||||
|
blockPageSubscription: Subscription;
|
||||||
networkSubscription: Subscription;
|
networkSubscription: Subscription;
|
||||||
tabHiddenSubscription: Subscription;
|
tabHiddenSubscription: Subscription;
|
||||||
markBlockSubscription: Subscription;
|
markBlockSubscription: Subscription;
|
||||||
@ -31,7 +43,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
arrowVisible = false;
|
arrowVisible = false;
|
||||||
arrowLeftPx = 30;
|
arrowLeftPx = 30;
|
||||||
blocksFilled = false;
|
blocksFilled = false;
|
||||||
transition = '1s';
|
arrowTransition = '1s';
|
||||||
showMiningInfo = false;
|
showMiningInfo = false;
|
||||||
timeLtrSubscription: Subscription;
|
timeLtrSubscription: Subscription;
|
||||||
timeLtr: boolean;
|
timeLtr: boolean;
|
||||||
@ -47,6 +59,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
|
public cacheService: CacheService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
private location: Location,
|
private location: Location,
|
||||||
) {
|
) {
|
||||||
@ -58,6 +71,8 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.dynamicBlocksAmount = Math.min(8, this.stateService.env.KEEP_BLOCKS_AMOUNT);
|
||||||
|
|
||||||
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
if (['', 'testnet', 'signet'].includes(this.stateService.network)) {
|
||||||
this.enabledMiningInfoIfNeeded(this.location.path());
|
this.enabledMiningInfoIfNeeded(this.location.path());
|
||||||
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
|
this.location.onUrlChange((url) => this.enabledMiningInfoIfNeeded(url));
|
||||||
@ -75,44 +90,52 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
this.loadingBlocks$ = this.stateService.isLoadingWebSocket$;
|
this.loadingBlocks$ = this.stateService.isLoadingWebSocket$;
|
||||||
this.networkSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
this.networkSubscription = this.stateService.networkChanged$.subscribe((network) => this.network = network);
|
||||||
this.tabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
this.tabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
|
||||||
this.blocksSubscription = this.stateService.blocks$
|
if (!this.static) {
|
||||||
.subscribe(([block, txConfirmed]) => {
|
this.blocksSubscription = this.stateService.blocks$
|
||||||
if (this.blocks.some((b) => b.height === block.height)) {
|
.subscribe(([block, txConfirmed]) => {
|
||||||
return;
|
if (this.blocks.some((b) => b.height === block.height)) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.blocks.length && block.height !== this.blocks[0].height + 1) {
|
if (this.blocks.length && block.height !== this.blocks[0].height + 1) {
|
||||||
this.blocks = [];
|
this.blocks = [];
|
||||||
this.blocksFilled = false;
|
this.blocksFilled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.blocks.unshift(block);
|
this.blocks.unshift(block);
|
||||||
this.blocks = this.blocks.slice(0, this.stateService.env.KEEP_BLOCKS_AMOUNT);
|
this.blocks = this.blocks.slice(0, this.dynamicBlocksAmount);
|
||||||
|
|
||||||
if (this.blocksFilled && !this.tabHidden && block.extras) {
|
if (txConfirmed) {
|
||||||
block.extras.stage = block.extras.matchRate >= 66 ? 1 : 2;
|
this.markHeight = block.height;
|
||||||
}
|
this.moveArrowToPosition(true, true);
|
||||||
|
} else {
|
||||||
|
this.moveArrowToPosition(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
if (txConfirmed) {
|
|
||||||
this.markHeight = block.height;
|
|
||||||
this.moveArrowToPosition(true, true);
|
|
||||||
} else {
|
|
||||||
this.moveArrowToPosition(true, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.blockStyles = [];
|
|
||||||
this.blocks.forEach((b) => this.blockStyles.push(this.getStyleForBlock(b)));
|
|
||||||
setTimeout(() => {
|
|
||||||
this.blockStyles = [];
|
this.blockStyles = [];
|
||||||
this.blocks.forEach((b) => this.blockStyles.push(this.getStyleForBlock(b)));
|
if (this.blocksFilled) {
|
||||||
this.cd.markForCheck();
|
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, i ? -155 : -205)));
|
||||||
}, 50);
|
setTimeout(() => {
|
||||||
|
this.blockStyles = [];
|
||||||
|
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}, 50);
|
||||||
|
} else {
|
||||||
|
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
|
||||||
|
}
|
||||||
|
|
||||||
if (this.blocks.length === this.stateService.env.KEEP_BLOCKS_AMOUNT) {
|
if (this.blocks.length === this.dynamicBlocksAmount) {
|
||||||
this.blocksFilled = true;
|
this.blocksFilled = true;
|
||||||
|
}
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.blockPageSubscription = this.cacheService.loadedBlocks$.subscribe((block) => {
|
||||||
|
if (block.height <= this.height && block.height > this.height - this.count) {
|
||||||
|
this.onBlockLoaded(block);
|
||||||
}
|
}
|
||||||
this.cd.markForCheck();
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.markBlockSubscription = this.stateService.markBlock$
|
this.markBlockSubscription = this.stateService.markBlock$
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
@ -123,10 +146,26 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
this.moveArrowToPosition(false);
|
this.moveArrowToPosition(false);
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.static) {
|
||||||
|
this.updateStaticBlocks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (this.static) {
|
||||||
|
const animateSlide = changes.height && (changes.height.currentValue === changes.height.previousValue + 1);
|
||||||
|
this.updateStaticBlocks(animateSlide);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.blocksSubscription.unsubscribe();
|
if (this.blocksSubscription) {
|
||||||
|
this.blocksSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
if (this.blockPageSubscription) {
|
||||||
|
this.blockPageSubscription.unsubscribe();
|
||||||
|
}
|
||||||
this.networkSubscription.unsubscribe();
|
this.networkSubscription.unsubscribe();
|
||||||
this.tabHiddenSubscription.unsubscribe();
|
this.tabHiddenSubscription.unsubscribe();
|
||||||
this.markBlockSubscription.unsubscribe();
|
this.markBlockSubscription.unsubscribe();
|
||||||
@ -142,13 +181,13 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight);
|
const blockindex = this.blocks.findIndex((b) => b.height === this.markHeight);
|
||||||
if (blockindex > -1) {
|
if (blockindex > -1) {
|
||||||
if (!animate) {
|
if (!animate) {
|
||||||
this.transition = 'inherit';
|
this.arrowTransition = 'inherit';
|
||||||
}
|
}
|
||||||
this.arrowVisible = true;
|
this.arrowVisible = true;
|
||||||
if (newBlockFromLeft) {
|
if (newBlockFromLeft) {
|
||||||
this.arrowLeftPx = blockindex * 155 + 30 - 205;
|
this.arrowLeftPx = blockindex * 155 + 30 - 205;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.transition = '2s';
|
this.arrowTransition = '2s';
|
||||||
this.arrowLeftPx = blockindex * 155 + 30;
|
this.arrowLeftPx = blockindex * 155 + 30;
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
}, 50);
|
}, 50);
|
||||||
@ -156,45 +195,117 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
this.arrowLeftPx = blockindex * 155 + 30;
|
this.arrowLeftPx = blockindex * 155 + 30;
|
||||||
if (!animate) {
|
if (!animate) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.transition = '2s';
|
this.arrowTransition = '2s';
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
});
|
}, 50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.arrowVisible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trackByBlocksFn(index: number, item: BlockExtended) {
|
trackByBlocksFn(index: number, item: BlockchainBlock) {
|
||||||
return item.height;
|
return item.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStyleForBlock(block: BlockExtended) {
|
updateStaticBlocks(animateSlide: boolean = false) {
|
||||||
|
// reset blocks
|
||||||
|
this.blocks = [];
|
||||||
|
this.blockStyles = [];
|
||||||
|
while (this.blocks.length < this.count) {
|
||||||
|
const height = this.height - this.blocks.length;
|
||||||
|
let block;
|
||||||
|
if (height >= 0) {
|
||||||
|
this.cacheService.loadBlock(height);
|
||||||
|
block = this.cacheService.getCachedBlock(height) || null;
|
||||||
|
}
|
||||||
|
this.blocks.push(block || {
|
||||||
|
placeholder: height < 0,
|
||||||
|
loading: height >= 0,
|
||||||
|
id: '',
|
||||||
|
height,
|
||||||
|
version: 0,
|
||||||
|
timestamp: 0,
|
||||||
|
bits: 0,
|
||||||
|
nonce: 0,
|
||||||
|
difficulty: 0,
|
||||||
|
merkle_root: '',
|
||||||
|
tx_count: 0,
|
||||||
|
size: 0,
|
||||||
|
weight: 0,
|
||||||
|
previousblockhash: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.blocks = this.blocks.slice(0, this.count);
|
||||||
|
this.blockStyles = [];
|
||||||
|
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i, animateSlide ? -155 : 0)));
|
||||||
|
this.cd.markForCheck();
|
||||||
|
if (animateSlide) {
|
||||||
|
// animate blocks slide right
|
||||||
|
setTimeout(() => {
|
||||||
|
this.blockStyles = [];
|
||||||
|
this.blocks.forEach((b, i) => this.blockStyles.push(this.getStyleForBlock(b, i)));
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}, 50);
|
||||||
|
this.moveArrowToPosition(true, true);
|
||||||
|
} else {
|
||||||
|
this.moveArrowToPosition(false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlockLoaded(block: BlockExtended) {
|
||||||
|
const blockIndex = this.height - block.height;
|
||||||
|
if (blockIndex >= 0 && blockIndex < this.blocks.length) {
|
||||||
|
this.blocks[blockIndex] = block;
|
||||||
|
this.blockStyles[blockIndex] = this.getStyleForBlock(block, blockIndex);
|
||||||
|
}
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
getStyleForBlock(block: BlockchainBlock, index: number, animateEnterFrom: number = 0) {
|
||||||
|
if (!block || block.placeholder) {
|
||||||
|
return this.getStyleForPlaceholderBlock(index, animateEnterFrom);
|
||||||
|
} else if (block.loading) {
|
||||||
|
return this.getStyleForLoadingBlock(index, animateEnterFrom);
|
||||||
|
}
|
||||||
const greenBackgroundHeight = 100 - (block.weight / this.stateService.env.BLOCK_WEIGHT_UNITS) * 100;
|
const greenBackgroundHeight = 100 - (block.weight / this.stateService.env.BLOCK_WEIGHT_UNITS) * 100;
|
||||||
let addLeft = 0;
|
let addLeft = 0;
|
||||||
|
|
||||||
if (block?.extras?.stage === 1) {
|
if (animateEnterFrom) {
|
||||||
block.extras.stage = 2;
|
addLeft = animateEnterFrom || 0;
|
||||||
addLeft = -205;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
left: addLeft + 155 * this.blocks.indexOf(block) + 'px',
|
left: addLeft + 155 * index + 'px',
|
||||||
background: `repeating-linear-gradient(
|
background: `repeating-linear-gradient(
|
||||||
#2d3348,
|
#2d3348,
|
||||||
#2d3348 ${greenBackgroundHeight}%,
|
#2d3348 ${greenBackgroundHeight}%,
|
||||||
${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%,
|
${this.gradientColors[this.network][0]} ${Math.max(greenBackgroundHeight, 0)}%,
|
||||||
${this.gradientColors[this.network][1]} 100%
|
${this.gradientColors[this.network][1]} 100%
|
||||||
)`,
|
)`,
|
||||||
|
transition: animateEnterFrom ? 'background 2s, transform 1s' : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getStyleForEmptyBlock(block: BlockExtended) {
|
getStyleForLoadingBlock(index: number, animateEnterFrom: number = 0) {
|
||||||
let addLeft = 0;
|
const addLeft = animateEnterFrom || 0;
|
||||||
|
|
||||||
if (block?.extras?.stage === 1) {
|
return {
|
||||||
block.extras.stage = 2;
|
left: addLeft + (155 * index) + 'px',
|
||||||
addLeft = -205;
|
background: "#2d3348",
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getStyleForPlaceholderBlock(index: number, animateEnterFrom: number = 0) {
|
||||||
|
const addLeft = animateEnterFrom || 0;
|
||||||
|
return {
|
||||||
|
left: addLeft + (155 * index) + 'px',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getStyleForEmptyBlock(block: BlockExtended, animateEnterFrom: number = 0) {
|
||||||
|
const addLeft = animateEnterFrom || 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
left: addLeft + 155 * this.emptyBlocks.indexOf(block) + 'px',
|
left: addLeft + 155 * this.emptyBlocks.indexOf(block) + 'px',
|
||||||
@ -204,7 +315,7 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
mountEmptyBlocks() {
|
mountEmptyBlocks() {
|
||||||
const emptyBlocks = [];
|
const emptyBlocks = [];
|
||||||
for (let i = 0; i < this.stateService.env.KEEP_BLOCKS_AMOUNT; i++) {
|
for (let i = 0; i < this.dynamicBlocksAmount; i++) {
|
||||||
emptyBlocks.push({
|
emptyBlocks.push({
|
||||||
id: '',
|
id: '',
|
||||||
height: 0,
|
height: 0,
|
||||||
@ -219,7 +330,6 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
|
|||||||
weight: 0,
|
weight: 0,
|
||||||
previousblockhash: '',
|
previousblockhash: '',
|
||||||
matchRate: 0,
|
matchRate: 0,
|
||||||
stage: 0,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return emptyBlocks;
|
return emptyBlocks;
|
||||||
|
@ -2,10 +2,14 @@
|
|||||||
<div class="position-container" [ngClass]="network ? network : ''">
|
<div class="position-container" [ngClass]="network ? network : ''">
|
||||||
<span>
|
<span>
|
||||||
<div class="blocks-wrapper">
|
<div class="blocks-wrapper">
|
||||||
<app-mempool-blocks></app-mempool-blocks>
|
<div class="scroll-spacer" *ngIf="minScrollWidth" [style.left]="minScrollWidth + 'px'"></div>
|
||||||
<app-blockchain-blocks></app-blockchain-blocks>
|
<app-mempool-blocks [hidden]="pageIndex > 0"></app-mempool-blocks>
|
||||||
|
<app-blockchain-blocks [hidden]="pageIndex > 0"></app-blockchain-blocks>
|
||||||
|
<ng-container *ngFor="let page of pages; trackBy: trackByPageFn">
|
||||||
|
<app-blockchain-blocks [static]="true" [offset]="page.offset" [height]="page.height" [count]="blocksPerPage"></app-blockchain-blocks>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div id="divider">
|
<div id="divider" [hidden]="pageIndex > 0">
|
||||||
<button class="time-toggle" (click)="toggleTimeDirection()"><fa-icon [icon]="['fas', 'exchange-alt']" [fixedWidth]="true"></fa-icon></button>
|
<button class="time-toggle" (click)="toggleTimeDirection()"><fa-icon [icon]="['fas', 'exchange-alt']" [fixedWidth]="true"></fa-icon></button>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user