Merge branch 'master' into natsoni/use-adjusted-time-avg

This commit is contained in:
softsimon 2024-12-21 22:10:04 +07:00 committed by GitHub
commit 24a76cafa4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
375 changed files with 6272 additions and 2623 deletions

View file

@ -46,7 +46,8 @@
"PASSWORD": "mempool", "PASSWORD": "mempool",
"TIMEOUT": 60000, "TIMEOUT": 60000,
"COOKIE": false, "COOKIE": false,
"COOKIE_PATH": "/path/to/bitcoin/.cookie" "COOKIE_PATH": "/path/to/bitcoin/.cookie",
"DEBUG_LOG_PATH": "/path/to/bitcoin/debug.log"
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "127.0.0.1", "HOST": "127.0.0.1",

View file

@ -16,7 +16,7 @@
"axios": "1.7.2", "axios": "1.7.2",
"bitcoinjs-lib": "~6.1.3", "bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0", "crypto-js": "~4.2.0",
"express": "~4.21.0", "express": "~4.21.1",
"maxmind": "~4.3.11", "maxmind": "~4.3.11",
"mysql2": "~3.11.0", "mysql2": "~3.11.0",
"redis": "^4.7.0", "redis": "^4.7.0",
@ -2827,9 +2827,9 @@
"dev": true "dev": true
}, },
"node_modules/cookie": { "node_modules/cookie": {
"version": "0.6.0", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@ -3461,16 +3461,16 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.21.0", "version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.7.1",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@ -9865,9 +9865,9 @@
"dev": true "dev": true
}, },
"cookie": { "cookie": {
"version": "0.6.0", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="
}, },
"cookie-signature": { "cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
@ -10319,16 +10319,16 @@
} }
}, },
"express": { "express": {
"version": "4.21.0", "version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"requires": { "requires": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.7.1",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",

View file

@ -45,7 +45,7 @@
"axios": "1.7.2", "axios": "1.7.2",
"bitcoinjs-lib": "~6.1.3", "bitcoinjs-lib": "~6.1.3",
"crypto-js": "~4.2.0", "crypto-js": "~4.2.0",
"express": "~4.21.0", "express": "~4.21.1",
"maxmind": "~4.3.11", "maxmind": "~4.3.11",
"mysql2": "~3.11.0", "mysql2": "~3.11.0",
"rust-gbt": "file:./rust-gbt", "rust-gbt": "file:./rust-gbt",

View file

@ -47,7 +47,8 @@
"PASSWORD": "__CORE_RPC_PASSWORD__", "PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": 1000, "TIMEOUT": 1000,
"COOKIE": false, "COOKIE": false,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__" "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__",
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "__ELECTRUM_HOST__", "HOST": "__ELECTRUM_HOST__",

View file

@ -74,7 +74,8 @@ describe('Mempool Backend Config', () => {
PASSWORD: 'mempool', PASSWORD: 'mempool',
TIMEOUT: 60000, TIMEOUT: 60000,
COOKIE: false, COOKIE: false,
COOKIE_PATH: '/bitcoin/.cookie' COOKIE_PATH: '/bitcoin/.cookie',
DEBUG_LOG_PATH: '',
}); });
expect(config.SECOND_CORE_RPC).toStrictEqual({ expect(config.SECOND_CORE_RPC).toStrictEqual({

View file

@ -1,4 +1,4 @@
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi { export interface AbstractBitcoinApi {
@ -23,12 +23,14 @@ export interface AbstractBitcoinApi {
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>; $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$sendRawTransaction(rawTransaction: string): Promise<string>; $sendRawTransaction(rawTransaction: string): Promise<string>;
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>; $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult>;
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>; $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>; $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>; $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>; $getBatchedOutspendsInternal(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
$getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>; $getOutSpendsByOutpoint(outpoints: { txid: string, vout: number }[]): Promise<IEsploraApi.Outspend[]>;
$getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>; $getCoinbaseTx(blockhash: string): Promise<IEsploraApi.Transaction>;
$getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]>;
startHealthChecks(): void; startHealthChecks(): void;
getHealthStatus(): HealthCheckHost[]; getHealthStatus(): HealthCheckHost[];

View file

@ -218,3 +218,21 @@ export interface TestMempoolAcceptResult {
}, },
['reject-reason']?: string, ['reject-reason']?: string,
} }
export interface SubmitPackageResult {
package_msg: string;
"tx-results": { [wtxid: string]: TxResult };
"replaced-transactions"?: string[];
}
export interface TxResult {
txid: string;
"other-wtxid"?: string;
vsize?: number;
fees?: {
base: number;
"effective-feerate"?: number;
"effective-includes"?: string[];
};
error?: string;
}

View file

@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib'; import * as bitcoinjs from 'bitcoinjs-lib';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks'; import blocks from '../blocks';
import mempool from '../mempool'; import mempool from '../mempool';
@ -196,6 +196,10 @@ class BitcoinApi implements AbstractBitcoinApi {
} }
} }
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult> {
return this.bitcoindClient.submitPackage(rawTransactions, maxfeerate ?? undefined, maxburnamount ?? undefined);
}
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
return { return {
@ -251,6 +255,10 @@ class BitcoinApi implements AbstractBitcoinApi {
return this.$getRawTransaction(txids[0]); return this.$getRawTransaction(txids[0]);
} }
async $getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]> {
throw new Error('Method getAddressTransactionSummary not supported by the Bitcoin RPC API.');
}
$getEstimatedHashrate(blockHeight: number): Promise<number> { $getEstimatedHashrate(blockHeight: number): Promise<number> {
// 120 is the default block span in Core // 120 is the default block span in Core
return this.bitcoindClient.getNetworkHashPs(120, blockHeight); return this.bitcoindClient.getNetworkHashPs(120, blockHeight);

View file

@ -1,6 +1,11 @@
import { Application, NextFunction, Request, Response } from 'express'; import { Application, NextFunction, Request, Response } from 'express';
import logger from '../../logger'; import logger from '../../logger';
import bitcoinClient from './bitcoin-client'; import bitcoinClient from './bitcoin-client';
import config from '../../config';
const BLOCKHASH_REGEX = /^[a-f0-9]{64}$/i;
const TXID_REGEX = /^[a-f0-9]{64}$/i;
const RAW_TX_REGEX = /^[a-f0-9]{2,}$/i;
/** /**
* Define a set of routes used by the accelerator server * Define a set of routes used by the accelerator server
@ -9,17 +14,17 @@ import bitcoinClient from './bitcoin-client';
class BitcoinBackendRoutes { class BitcoinBackendRoutes {
private static tag = 'BitcoinBackendRoutes'; private static tag = 'BitcoinBackendRoutes';
public initRoutes(app: Application) { public initRoutes(app: Application): void {
app app
.get('/api/internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry) .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
.post('/api/internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction) .post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
.get('/api/internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction) .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
.post('/api/internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction) .post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
.post('/api/internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept) .post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
.get('/api/internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors) .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
.get('/api/internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
.get('/api/internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash) .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash)
.get('/api/internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount) .get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount)
; ;
} }
@ -46,9 +51,9 @@ class BitcoinBackendRoutes {
*/ */
private static handleException(e: any, fnName: string, res: Response): void { private static handleException(e: any, fnName: string, res: Response): void {
if (typeof(e.code) === 'number') { if (typeof(e.code) === 'number') {
res.status(400).send(JSON.stringify(e, ['code', 'message'])); res.status(400).send(JSON.stringify(e, ['code']));
} else { } else {
const err = `exception in ${fnName}. ${e}. Details: ${JSON.stringify(e, ['code', 'message'])}`; const err = `unknown exception in ${fnName}`;
logger.err(err, BitcoinBackendRoutes.tag); logger.err(err, BitcoinBackendRoutes.tag);
res.status(500).send(err); res.status(500).send(err);
} }
@ -57,13 +62,13 @@ class BitcoinBackendRoutes {
private async $getMempoolEntry(req: Request, res: Response): Promise<void> { private async $getMempoolEntry(req: Request, res: Response): Promise<void> {
const txid = req.query.txid; const txid = req.query.txid;
try { try {
if (typeof(txid) !== 'string' || txid.length !== 64) { if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
return; return;
} }
const mempoolEntry = await bitcoinClient.getMempoolEntry(txid); const mempoolEntry = await bitcoinClient.getMempoolEntry(txid);
if (!mempoolEntry) { if (!mempoolEntry) {
res.status(404).send(`no mempool entry found for txid ${txid}`); res.status(404).send();
return; return;
} }
res.status(200).send(mempoolEntry); res.status(200).send(mempoolEntry);
@ -75,13 +80,13 @@ class BitcoinBackendRoutes {
private async $decodeRawTransaction(req: Request, res: Response): Promise<void> { private async $decodeRawTransaction(req: Request, res: Response): Promise<void> {
const rawTx = req.body.rawTx; const rawTx = req.body.rawTx;
try { try {
if (typeof(rawTx) !== 'string') { if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) {
res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`); res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`);
return; return;
} }
const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx); const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx);
if (!decodedTx) { if (!decodedTx) {
res.status(400).send(`unable to decode rawTx ${rawTx}`); res.status(400).send(`unable to decode rawTx`);
return; return;
} }
res.status(200).send(decodedTx); res.status(200).send(decodedTx);
@ -94,23 +99,23 @@ class BitcoinBackendRoutes {
const txid = req.query.txid; const txid = req.query.txid;
const verbose = req.query.verbose; const verbose = req.query.verbose;
try { try {
if (typeof(txid) !== 'string' || txid.length !== 64) { if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
return; return;
} }
if (typeof(verbose) !== 'string') { if (typeof(verbose) !== 'string') {
res.status(400).send(`invalid param verbose ${verbose}. must be a string representing an integer`); res.status(400).send(`invalid param verbose. must be a string representing an integer`);
return; return;
} }
const verboseNumber = parseInt(verbose, 10); const verboseNumber = parseInt(verbose, 10);
if (typeof(verboseNumber) !== 'number') { if (typeof(verboseNumber) !== 'number') {
res.status(400).send(`invalid param verbose ${verbose}. must be a valid integer`); res.status(400).send(`invalid param verbose. must be a valid integer`);
return; return;
} }
const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber); const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber);
if (!decodedTx) { if (!decodedTx) {
res.status(400).send(`unable to get raw transaction for txid ${txid}`); res.status(400).send(`unable to get raw transaction`);
return; return;
} }
res.status(200).send(decodedTx); res.status(200).send(decodedTx);
@ -122,13 +127,13 @@ class BitcoinBackendRoutes {
private async $sendRawTransaction(req: Request, res: Response): Promise<void> { private async $sendRawTransaction(req: Request, res: Response): Promise<void> {
const rawTx = req.body.rawTx; const rawTx = req.body.rawTx;
try { try {
if (typeof(rawTx) !== 'string') { if (typeof(rawTx) !== 'string' || !RAW_TX_REGEX.test(rawTx)) {
res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`); res.status(400).send(`invalid param rawTx. must be a string of hexadecimal characters`);
return; return;
} }
const txHex = await bitcoinClient.sendRawTransaction(rawTx); const txHex = await bitcoinClient.sendRawTransaction(rawTx);
if (!txHex) { if (!txHex) {
res.status(400).send(`unable to send rawTx ${rawTx}`); res.status(400).send(`unable to send rawTx`);
return; return;
} }
res.status(200).send(txHex); res.status(200).send(txHex);
@ -140,13 +145,13 @@ class BitcoinBackendRoutes {
private async $testMempoolAccept(req: Request, res: Response): Promise<void> { private async $testMempoolAccept(req: Request, res: Response): Promise<void> {
const rawTxs = req.body.rawTxs; const rawTxs = req.body.rawTxs;
try { try {
if (typeof(rawTxs) !== 'object') { if (typeof(rawTxs) !== 'object' || !Array.isArray(rawTxs) || rawTxs.some((tx) => typeof(tx) !== 'string' || !RAW_TX_REGEX.test(tx))) {
res.status(400).send(`invalid param rawTxs ${JSON.stringify(rawTxs)}. must be an array of string`); res.status(400).send(`invalid param rawTxs. must be an array of strings of hexadecimal characters`);
return; return;
} }
const txHex = await bitcoinClient.testMempoolAccept(rawTxs); const txHex = await bitcoinClient.testMempoolAccept(rawTxs);
if (typeof(txHex) !== 'object' || txHex.length === 0) { if (typeof(txHex) !== 'object' || txHex.length === 0) {
res.status(400).send(`testmempoolaccept failed for raw txs ${JSON.stringify(rawTxs)}, got an empty result`); res.status(400).send(`testmempoolaccept failed for raw txs, got an empty result`);
return; return;
} }
res.status(200).send(txHex); res.status(200).send(txHex);
@ -159,18 +164,18 @@ class BitcoinBackendRoutes {
const txid = req.query.txid; const txid = req.query.txid;
const verbose = req.query.verbose; const verbose = req.query.verbose;
try { try {
if (typeof(txid) !== 'string' || txid.length !== 64) { if (typeof(txid) !== 'string' || txid.length !== 64 || !TXID_REGEX.test(txid)) {
res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); res.status(400).send(`invalid param txid. must be 64 hexadecimal characters`);
return; return;
} }
if (typeof(verbose) !== 'string' || (verbose !== 'true' && verbose !== 'false')) { if (typeof(verbose) !== 'string' || (verbose !== 'true' && verbose !== 'false')) {
res.status(400).send(`invalid param verbose ${verbose}. must be a string ('true' | 'false')`); res.status(400).send(`invalid param verbose. must be a string ('true' | 'false')`);
return; return;
} }
const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false); const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false);
if (!ancestors) { if (!ancestors) {
res.status(400).send(`unable to get mempool ancestors for txid ${txid}`); res.status(400).send(`unable to get mempool ancestors`);
return; return;
} }
res.status(200).send(ancestors); res.status(200).send(ancestors);
@ -183,23 +188,23 @@ class BitcoinBackendRoutes {
const blockHash = req.query.hash; const blockHash = req.query.hash;
const verbosity = req.query.verbosity; const verbosity = req.query.verbosity;
try { try {
if (typeof(blockHash) !== 'string' || blockHash.length !== 64) { if (typeof(blockHash) !== 'string' || blockHash.length !== 64 || !BLOCKHASH_REGEX.test(blockHash)) {
res.status(400).send(`invalid param blockHash ${blockHash}. must be a string of 64 char`); res.status(400).send(`invalid param blockHash. must be 64 hexadecimal characters`);
return; return;
} }
if (typeof(verbosity) !== 'string') { if (typeof(verbosity) !== 'string') {
res.status(400).send(`invalid param verbosity ${verbosity}. must be a string representing an integer`); res.status(400).send(`invalid param verbosity. must be a string representing an integer`);
return; return;
} }
const verbosityNumber = parseInt(verbosity, 10); const verbosityNumber = parseInt(verbosity, 10);
if (typeof(verbosityNumber) !== 'number') { if (typeof(verbosityNumber) !== 'number') {
res.status(400).send(`invalid param verbosity ${verbosity}. must be a valid integer`); res.status(400).send(`invalid param verbosity. must be a valid integer`);
return; return;
} }
const block = await bitcoinClient.getBlock(blockHash, verbosityNumber); const block = await bitcoinClient.getBlock(blockHash, verbosityNumber);
if (!block) { if (!block) {
res.status(400).send(`unable to get block for block hash ${blockHash}`); res.status(400).send(`unable to get block`);
return; return;
} }
res.status(200).send(block); res.status(200).send(block);
@ -212,18 +217,18 @@ class BitcoinBackendRoutes {
const blockHeight = req.query.height; const blockHeight = req.query.height;
try { try {
if (typeof(blockHeight) !== 'string') { if (typeof(blockHeight) !== 'string') {
res.status(400).send(`invalid param blockHeight ${blockHeight}, must be a string representing an integer`); res.status(400).send(`invalid param blockHeight, must be a string representing an integer`);
return; return;
} }
const blockHeightNumber = parseInt(blockHeight, 10); const blockHeightNumber = parseInt(blockHeight, 10);
if (typeof(blockHeightNumber) !== 'number') { if (typeof(blockHeightNumber) !== 'number') {
res.status(400).send(`invalid param blockHeight ${blockHeight}. must be a valid integer`); res.status(400).send(`invalid param blockHeight. must be a valid integer`);
return; return;
} }
const block = await bitcoinClient.getBlockHash(blockHeightNumber); const block = await bitcoinClient.getBlockHash(blockHeightNumber);
if (!block) { if (!block) {
res.status(400).send(`unable to get block hash for block height ${blockHeightNumber}`); res.status(400).send(`unable to get block hash`);
return; return;
} }
res.status(200).send(block); res.status(200).send(block);
@ -246,4 +251,4 @@ class BitcoinBackendRoutes {
} }
} }
export default new BitcoinBackendRoutes export default new BitcoinBackendRoutes;

View file

@ -22,6 +22,11 @@ import rbfCache from '../rbf-cache';
import { calculateMempoolTxCpfp } from '../cpfp'; import { calculateMempoolTxCpfp } from '../cpfp';
import { handleError } from '../../utils/api'; import { handleError } from '../../utils/api';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
const BLOCK_HASH_REGEX = /^[a-f0-9]{64}$/i;
const ADDRESS_REGEX = /^[a-z0-9]{2,120}$/i;
const SCRIPT_HASH_REGEX = /^([a-f0-9]{2})+$/i;
class BitcoinRoutes { class BitcoinRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
app app
@ -42,12 +47,15 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/summary', this.getStrippedBlockTransactions)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/summary', this.getStrippedBlockTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/audit-summary', this.getBlockAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/tx/:txid/audit', this.$getBlockTxAuditSummary)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
// Temporarily add txs/package endpoint for all backends until esplora supports it
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
; ;
if (config.MEMPOOL.BACKEND !== 'esplora') { if (config.MEMPOOL.BACKEND !== 'esplora') {
@ -87,7 +95,7 @@ class BitcoinRoutes {
res.set('Content-Type', 'application/json'); res.set('Content-Type', 'application/json');
res.send(result); res.send(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get init data');
} }
} }
@ -106,7 +114,7 @@ class BitcoinRoutes {
const result = mempoolBlocks.getMempoolBlocks(); const result = mempoolBlocks.getMempoolBlocks();
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get mempool blocks');
} }
} }
@ -118,7 +126,10 @@ class BitcoinRoutes {
const txIds: string[] = []; const txIds: string[] = [];
for (const _txId in req.query.txId) { for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') { if (typeof req.query.txId[_txId] === 'string') {
txIds.push(req.query.txId[_txId].toString()); const txid = req.query.txId[_txId].toString();
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
} }
} }
@ -137,18 +148,22 @@ class BitcoinRoutes {
handleError(req, res, 400, 'Too many txids requested'); handleError(req, res, 400, 'Too many txids requested');
return; return;
} }
if (txids.some((txid) => !TXID_REGEX.test(txid))) {
handleError(req, res, 400, 'Invalid txids format');
return;
}
try { try {
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids); const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
res.json(batchedOutspends); res.json(batchedOutspends);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get batched outspends');
} }
} }
private async $getCpfpInfo(req: Request, res: Response) { private async $getCpfpInfo(req: Request, res: Response) {
if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) { if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID.`); handleError(req, res, 501, `Invalid transaction ID`);
return; return;
} }
@ -181,7 +196,7 @@ class BitcoinRoutes {
try { try {
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId); cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
} catch (e) { } catch (e) {
handleError(req, res, 500, 'failed to get CPFP info'); handleError(req, res, 500, 'Failed to get CPFP info');
return; return;
} }
} }
@ -202,6 +217,10 @@ class BitcoinRoutes {
} }
private async getTransaction(req: Request, res: Response) { private async getTransaction(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true); const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true, false, false, true);
res.json(transaction); res.json(transaction);
@ -209,12 +228,17 @@ class BitcoinRoutes {
let statusCode = 500; let statusCode = 500;
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
} }
handleError(req, res, statusCode, e instanceof Error ? e.message : e); handleError(req, res, statusCode, 'Failed to get transaction');
} }
} }
private async getRawTransaction(req: Request, res: Response) { private async getRawTransaction(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true); const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true);
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
@ -223,8 +247,9 @@ class BitcoinRoutes {
let statusCode = 500; let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
} }
handleError(req, res, statusCode, e instanceof Error ? e.message : e); handleError(req, res, statusCode, 'Failed to get raw transaction');
} }
} }
@ -289,14 +314,18 @@ class BitcoinRoutes {
} }
} catch (e: any) { } catch (e: any) {
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) { if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
handleError(req, res, 404, e.message); handleError(req, res, 404, notFoundError);
} else { } else {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to process PSBT');
} }
} }
} }
private async getTransactionStatus(req: Request, res: Response) { private async getTransactionStatus(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true); const transaction = await transactionUtils.$getTransactionExtended(req.params.txId, true);
res.json(transaction.status); res.json(transaction.status);
@ -304,22 +333,53 @@ class BitcoinRoutes {
let statusCode = 500; let statusCode = 500;
if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) { if (e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404; statusCode = 404;
handleError(req, res, statusCode, 'No such mempool or blockchain transaction');
} }
handleError(req, res, statusCode, e instanceof Error ? e.message : e); handleError(req, res, statusCode, 'Failed to get transaction status');
} }
} }
private async getStrippedBlockTransactions(req: Request, res: Response) { private async getStrippedBlockTransactions(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash); const transactions = await blocks.$getStrippedBlockTransactions(req.params.hash);
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get block summary');
}
}
private async getStrippedBlockTransaction(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
if (!TXID_REGEX.test(req.params.txid)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try {
const transaction = await blocks.$getSingleTxFromSummary(req.params.hash, req.params.txid);
if (!transaction) {
handleError(req, res, 404, `Transaction not found in summary`);
return;
}
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(transaction);
} catch (e) {
handleError(req, res, 500, 'Failed to get transaction from summary');
} }
} }
private async getBlock(req: Request, res: Response) { private async getBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const block = await blocks.$getBlock(req.params.hash); const block = await blocks.$getBlock(req.params.hash);
@ -331,53 +391,69 @@ class BitcoinRoutes {
} else if (blockAge > 30 * day) { } else if (blockAge > 30 * day) {
cacheDuration = 10 * day; cacheDuration = 10 * day;
} else { } else {
cacheDuration = 600 cacheDuration = 600;
} }
res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * cacheDuration).toUTCString());
res.json(block); res.json(block);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get block');
} }
} }
private async getBlockHeader(req: Request, res: Response) { private async getBlockHeader(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash); const blockHeader = await bitcoinApi.$getBlockHeader(req.params.hash);
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(blockHeader); res.send(blockHeader);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get block header');
} }
} }
private async getBlockAuditSummary(req: Request, res: Response) { private async getBlockAuditSummary(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash); const auditSummary = await blocks.$getBlockAuditSummary(req.params.hash);
if (auditSummary) { if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary); res.json(auditSummary);
} else { } else {
handleError(req, res, 404, `audit not available`); handleError(req, res, 404, `Audit not available`);
return; return;
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get block audit summary');
} }
} }
private async $getBlockTxAuditSummary(req: Request, res: Response) { private async $getBlockTxAuditSummary(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
if (!TXID_REGEX.test(req.params.txid)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid); const auditSummary = await blocks.$getBlockTxAuditSummary(req.params.hash, req.params.txid);
if (auditSummary) { if (auditSummary) {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
res.json(auditSummary); res.json(auditSummary);
} else { } else {
handleError(req, res, 404, `transaction audit not available`); handleError(req, res, 404, `Transaction audit not available`);
return; return;
} }
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get transaction audit summary');
} }
} }
@ -391,7 +467,7 @@ class BitcoinRoutes {
return await this.getLegacyBlocks(req, res); return await this.getLegacyBlocks(req, res);
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get blocks');
} }
} }
@ -433,7 +509,7 @@ class BitcoinRoutes {
res.json(await blocks.$getBlocksBetweenHeight(from, to)); res.json(await blocks.$getBlocksBetweenHeight(from, to));
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get blocks');
} }
} }
@ -468,11 +544,15 @@ class BitcoinRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(returnBlocks); res.json(returnBlocks);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get blocks');
} }
} }
private async getBlockTransactions(req: Request, res: Response) { private async getBlockTransactions(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0); loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
@ -493,7 +573,7 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100); loadingIndicators.setProgress('blocktxs-' + req.params.hash, 100);
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get block transactions');
} }
} }
@ -502,7 +582,7 @@ class BitcoinRoutes {
const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10)); const blockHash = await bitcoinApi.$getBlockHash(parseInt(req.params.height, 10));
res.send(blockHash); res.send(blockHash);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get block at height');
} }
} }
@ -511,16 +591,20 @@ class BitcoinRoutes {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
try { try {
const addressData = await bitcoinApi.$getAddress(req.params.address); const addressData = await bitcoinApi.$getAddress(req.params.address);
res.json(addressData); res.json(addressData);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e instanceof Error ? e.message : e); handleError(req, res, 413, e.message);
return; return;
} }
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get address');
} }
} }
@ -529,6 +613,10 @@ class BitcoinRoutes {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
try { try {
let lastTxId: string = ''; let lastTxId: string = '';
@ -539,10 +627,10 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e instanceof Error ? e.message : e); handleError(req, res, 413, e.message);
return; return;
} }
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get address transactions');
} }
} }
@ -558,6 +646,10 @@ class BitcoinRoutes {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
return;
}
try { try {
// electrum expects scripthashes in little-endian // electrum expects scripthashes in little-endian
@ -566,10 +658,10 @@ class BitcoinRoutes {
res.json(addressData); res.json(addressData);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e instanceof Error ? e.message : e); handleError(req, res, 413, e.message);
return; return;
} }
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get script hash');
} }
} }
@ -578,6 +670,10 @@ class BitcoinRoutes {
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.'); handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
return; return;
} }
if (!SCRIPT_HASH_REGEX.test(req.params.scripthash)) {
handleError(req, res, 501, `Invalid scripthash`);
return;
}
try { try {
// electrum expects scripthashes in little-endian // electrum expects scripthashes in little-endian
@ -590,10 +686,10 @@ class BitcoinRoutes {
res.json(transactions); res.json(transactions);
} catch (e) { } catch (e) {
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) { if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
handleError(req, res, 413, e instanceof Error ? e.message : e); handleError(req, res, 413, e.message);
return; return;
} }
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get script hash transactions');
} }
} }
@ -606,10 +702,10 @@ class BitcoinRoutes {
private async getAddressPrefix(req: Request, res: Response) { private async getAddressPrefix(req: Request, res: Response) {
try { try {
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix); const addressPrefix = await bitcoinApi.$getAddressPrefix(req.params.prefix);
res.send(blockHash); res.send(addressPrefix);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get address prefix');
} }
} }
@ -650,7 +746,7 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(result.toString()); res.send(result.toString());
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get height at tip');
} }
} }
@ -660,39 +756,55 @@ class BitcoinRoutes {
res.setHeader('content-type', 'text/plain'); res.setHeader('content-type', 'text/plain');
res.send(result); res.send(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get hash at tip');
} }
} }
private async getRawBlock(req: Request, res: Response) { private async getRawBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const result = await bitcoinApi.$getRawBlock(req.params.hash); const result = await bitcoinApi.$getRawBlock(req.params.hash);
res.setHeader('content-type', 'application/octet-stream'); res.setHeader('content-type', 'application/octet-stream');
res.send(result); res.send(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get raw block');
} }
} }
private async getTxIdsForBlock(req: Request, res: Response) { private async getTxIdsForBlock(req: Request, res: Response) {
if (!BLOCK_HASH_REGEX.test(req.params.hash)) {
handleError(req, res, 501, `Invalid block hash`);
return;
}
try { try {
const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get txids for block');
} }
} }
private async validateAddress(req: Request, res: Response) { private async validateAddress(req: Request, res: Response) {
if (!ADDRESS_REGEX.test(req.params.address)) {
handleError(req, res, 501, `Invalid address`);
return;
}
try { try {
const result = await bitcoinClient.validateAddress(req.params.address); const result = await bitcoinClient.validateAddress(req.params.address);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to validate address');
} }
} }
private async getRbfHistory(req: Request, res: Response) { private async getRbfHistory(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const replacements = rbfCache.getRbfTree(req.params.txId) || null; const replacements = rbfCache.getRbfTree(req.params.txId) || null;
const replaces = rbfCache.getReplaces(req.params.txId) || null; const replaces = rbfCache.getReplaces(req.params.txId) || null;
@ -701,7 +813,7 @@ class BitcoinRoutes {
replaces replaces
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get rbf history');
} }
} }
@ -710,7 +822,7 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(false); const result = rbfCache.getRbfTrees(false);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get rbf trees');
} }
} }
@ -719,11 +831,15 @@ class BitcoinRoutes {
const result = rbfCache.getRbfTrees(true); const result = rbfCache.getRbfTrees(true);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get full rbf replacements');
} }
} }
private async getCachedTx(req: Request, res: Response) { private async getCachedTx(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const result = rbfCache.getTx(req.params.txId); const result = rbfCache.getTx(req.params.txId);
if (result) { if (result) {
@ -732,16 +848,20 @@ class BitcoinRoutes {
res.status(204).send(); res.status(204).send();
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get cached tx');
} }
} }
private async getTransactionOutspends(req: Request, res: Response) { private async getTransactionOutspends(req: Request, res: Response) {
if (!TXID_REGEX.test(req.params.txId)) {
handleError(req, res, 501, `Invalid transaction ID`);
return;
}
try { try {
const result = await bitcoinApi.$getOutspends(req.params.txId); const result = await bitcoinApi.$getOutspends(req.params.txId);
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get transaction outspends');
} }
} }
@ -754,7 +874,7 @@ class BitcoinRoutes {
handleError(req, res, 503, `Service Temporarily Unavailable`); handleError(req, res, 503, `Service Temporarily Unavailable`);
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get difficulty change');
} }
} }
@ -765,8 +885,8 @@ class BitcoinRoutes {
const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx); const txIdResult = await bitcoinApi.$sendRawTransaction(rawTx);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } catch (e: any) {
handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code })
: (e.message || 'Error')); : 'Failed to send raw transaction');
} }
} }
@ -777,8 +897,8 @@ class BitcoinRoutes {
const txIdResult = await bitcoinClient.sendRawTransaction(txHex); const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
res.send(txIdResult); res.send(txIdResult);
} catch (e: any) { } catch (e: any) {
handleError(req, res, 400, e.message && e.code ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) handleError(req, res, 400, (e.message && e.code) ? 'sendrawtransaction RPC error: ' + JSON.stringify({ code: e.code })
: (e.message || 'Error')); : 'Failed to send raw transaction');
} }
} }
@ -789,8 +909,21 @@ class BitcoinRoutes {
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
res.send(result); res.send(result);
} catch (e: any) { } catch (e: any) {
handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) handleError(req, res, 400, (e.message && e.code) ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code })
: (e.message || 'Error')); : 'Failed to test transactions');
}
}
private async $submitPackage(req: Request, res: Response) {
try {
const rawTxs = Common.getTransactionsFromRequest(req);
const maxfeerate = parseFloat(req.query.maxfeerate as string);
const maxburnamount = parseFloat(req.query.maxburnamount as string);
const result = await bitcoinClient.submitPackage(rawTxs, maxfeerate ?? undefined, maxburnamount ?? undefined);
res.send(result);
} catch (e: any) {
handleError(req, res, 400, (e.message && e.code) ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code })
: 'Failed to submit package');
} }
} }

View file

@ -179,4 +179,11 @@ export namespace IEsploraApi {
burn_count: number; burn_count: number;
} }
export interface AddressTxSummary {
txid: string;
value: number;
height: number;
time: number;
tx_position?: number;
}
} }

View file

@ -1,12 +1,12 @@
import config from '../../config'; import config from '../../config';
import axios, { AxiosResponse, isAxiosError } from 'axios'; import axios, { isAxiosError } from 'axios';
import http from 'http'; import http from 'http';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger'; import logger from '../../logger';
import { Common } from '../common'; import { Common } from '../common';
import { TestMempoolAcceptResult } from './bitcoin-api.interface'; import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
import os from 'os';
interface FailoverHost { interface FailoverHost {
host: string, host: string,
rtts: number[], rtts: number[],
@ -20,6 +20,13 @@ interface FailoverHost {
preferred?: boolean, preferred?: boolean,
checked: boolean, checked: boolean,
lastChecked?: number, lastChecked?: number,
publicDomain: string,
hashes: {
frontend?: string,
backend?: string,
electrs?: string,
lastUpdated: number,
}
} }
class FailoverRouter { class FailoverRouter {
@ -29,14 +36,21 @@ class FailoverRouter {
maxHeight: number = 0; maxHeight: number = 0;
hosts: FailoverHost[]; hosts: FailoverHost[];
multihost: boolean; multihost: boolean;
pollInterval: number = 60000; gitHashInterval: number = 600000; // 10 minutes
pollInterval: number = 60000; // 1 minute
pollTimer: NodeJS.Timeout | null = null; pollTimer: NodeJS.Timeout | null = null;
pollConnection = axios.create(); pollConnection = axios.create();
localHostname: string = 'localhost';
requestConnection = axios.create({ requestConnection = axios.create({
httpAgent: new http.Agent({ keepAlive: true }) httpAgent: new http.Agent({ keepAlive: true })
}); });
constructor() { constructor() {
try {
this.localHostname = os.hostname();
} catch (e) {
logger.warn('Failed to set local hostname, using "localhost"');
}
// setup list of hosts // setup list of hosts
this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => { this.hosts = (config.ESPLORA.FALLBACK || []).map(domain => {
return { return {
@ -45,6 +59,10 @@ class FailoverRouter {
rtts: [], rtts: [],
rtt: Infinity, rtt: Infinity,
failures: 0, failures: 0,
publicDomain: 'https://' + this.extractPublicDomain(domain),
hashes: {
lastUpdated: 0,
},
}; };
}); });
this.activeHost = { this.activeHost = {
@ -55,6 +73,10 @@ class FailoverRouter {
socket: !!config.ESPLORA.UNIX_SOCKET_PATH, socket: !!config.ESPLORA.UNIX_SOCKET_PATH,
preferred: true, preferred: true,
checked: false, checked: false,
publicDomain: `http://${this.localHostname}`,
hashes: {
lastUpdated: 0,
},
}; };
this.fallbackHost = this.activeHost; this.fallbackHost = this.activeHost;
this.hosts.unshift(this.activeHost); this.hosts.unshift(this.activeHost);
@ -106,6 +128,24 @@ class FailoverRouter {
host.outOfSync = false; host.outOfSync = false;
} }
host.unreachable = false; host.unreachable = false;
// update esplora git hash using the x-powered-by header from the height check
const poweredBy = result.headers['x-powered-by'];
if (poweredBy) {
const match = poweredBy.match(/([a-fA-F0-9]{5,40})/);
if (match && match[1]?.length) {
host.hashes.electrs = match[1];
}
}
// Check front and backend git hashes less often
if (Date.now() - host.hashes.lastUpdated > this.gitHashInterval) {
await Promise.all([
this.$updateFrontendGitHash(host),
this.$updateBackendGitHash(host)
]);
host.hashes.lastUpdated = Date.now();
}
} else { } else {
host.outOfSync = true; host.outOfSync = true;
host.unreachable = true; host.unreachable = true;
@ -202,6 +242,47 @@ class FailoverRouter {
} }
} }
// methods for retrieving git hashes by host
private async $updateFrontendGitHash(host: FailoverHost): Promise<void> {
try {
const url = `${host.publicDomain}/resources/config.js`;
const response = await this.pollConnection.get<string>(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
const match = response.data.match(/GIT_COMMIT_HASH\s*=\s*['"](.*?)['"]/);
if (match && match[1]?.length) {
host.hashes.frontend = match[1];
}
} catch (e) {
// failed to get frontend build hash - do nothing
}
}
private async $updateBackendGitHash(host: FailoverHost): Promise<void> {
try {
const url = `${host.publicDomain}/api/v1/backend-info`;
const response = await this.pollConnection.get<any>(url, { timeout: config.ESPLORA.FALLBACK_TIMEOUT });
if (response.data?.gitCommit) {
host.hashes.backend = response.data.gitCommit;
}
} catch (e) {
// failed to get backend build hash - do nothing
}
}
// returns the public mempool domain corresponding to an esplora server url
// (a bit of a hack to avoid manually specifying frontend & backend URLs for each esplora server)
private extractPublicDomain(url: string): string {
// force the url to start with a valid protocol
const urlWithProtocol = url.startsWith('http') ? url : `https://${url}`;
// parse as URL and extract the hostname
try {
const parsed = new URL(urlWithProtocol);
return parsed.hostname;
} catch (e) {
// fallback to the original url
return url;
}
}
private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> { private async $query<T>(method: 'get'| 'post', path, data: any, responseType = 'json', host = this.activeHost, retry: boolean = true): Promise<T> {
let axiosConfig; let axiosConfig;
let url; let url;
@ -305,7 +386,7 @@ class ElectrsApi implements AbstractBitcoinApi {
} }
$getAddress(address: string): Promise<IEsploraApi.Address> { $getAddress(address: string): Promise<IEsploraApi.Address> {
throw new Error('Method getAddress not implemented.'); return this.failoverRouter.$get<IEsploraApi.Address>('/address/' + address);
} }
$getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> { $getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
@ -332,6 +413,10 @@ class ElectrsApi implements AbstractBitcoinApi {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
$submitPackage(rawTransactions: string[]): Promise<SubmitPackageResult> {
throw new Error('Method not implemented.');
}
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout); return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
} }
@ -357,6 +442,10 @@ class ElectrsApi implements AbstractBitcoinApi {
return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txid); return this.failoverRouter.$get<IEsploraApi.Transaction>('/tx/' + txid);
} }
async $getAddressTransactionSummary(address: string): Promise<IEsploraApi.AddressTxSummary[]> {
return this.failoverRouter.$get<IEsploraApi.AddressTxSummary[]>('/address/' + address + '/txs/summary');
}
public startHealthChecks(): void { public startHealthChecks(): void {
this.failoverRouter.startHealthChecks(); this.failoverRouter.startHealthChecks();
} }
@ -373,6 +462,7 @@ class ElectrsApi implements AbstractBitcoinApi {
unreachable: !!host.unreachable, unreachable: !!host.unreachable,
checked: !!host.checked, checked: !!host.checked,
lastChecked: host.lastChecked || 0, lastChecked: host.lastChecked || 0,
hashes: host.hashes,
})); }));
} else { } else {
return []; return [];

View file

@ -412,8 +412,16 @@ class Blocks {
} }
try { try {
const blockchainInfo = await bitcoinClient.getBlockchainInfo();
const currentBlockHeight = blockchainInfo.blocks;
let indexingBlockAmount = Math.min(config.MEMPOOL.INDEXING_BLOCKS_AMOUNT, currentBlockHeight);
if (indexingBlockAmount <= -1) {
indexingBlockAmount = currentBlockHeight + 1;
}
const lastBlockToIndex = Math.max(0, currentBlockHeight - indexingBlockAmount + 1);
// Get all indexed block hash // Get all indexed block hash
const indexedBlocks = await blocksRepository.$getIndexedBlocks(); const indexedBlocks = (await blocksRepository.$getIndexedBlocks()).filter(block => block.height >= lastBlockToIndex);
const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId(); const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
@ -1216,6 +1224,11 @@ class Blocks {
return summary.transactions; return summary.transactions;
} }
public async $getSingleTxFromSummary(hash: string, txid: string): Promise<TransactionClassified | null> {
const txs = await this.$getStrippedBlockTransactions(hash);
return txs.find(tx => tx.txid === txid) || null;
}
/** /**
* Get 15 blocks * Get 15 blocks
* *

View file

@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
class DatabaseMigration { class DatabaseMigration {
private static currentVersion = 82; private static currentVersion = 94;
private queryTimeout = 3600_000; private queryTimeout = 3600_000;
private statisticsAddedIndexed = false; private statisticsAddedIndexed = false;
private uniqueLogs: string[] = []; private uniqueLogs: string[] = [];
@ -705,6 +705,419 @@ class DatabaseMigration {
await this.$fixBadV1AuditBlocks(); await this.$fixBadV1AuditBlocks();
await this.updateToSchemaVersion(82); await this.updateToSchemaVersion(82);
} }
if (databaseSchemaVersion < 83 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
await this.updateToSchemaVersion(83);
}
// add new pools indexes
if (databaseSchemaVersion < 84 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`pools\`
ADD INDEX \`slug\` (\`slug\`),
ADD INDEX \`unique_id\` (\`unique_id\`)
`);
await this.updateToSchemaVersion(84);
}
// lightning channels indexes
if (databaseSchemaVersion < 85 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`channels\`
ADD INDEX \`created\` (\`created\`),
ADD INDEX \`capacity\` (\`capacity\`),
ADD INDEX \`closing_reason\` (\`closing_reason\`),
ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
`);
await this.updateToSchemaVersion(85);
}
// lightning nodes indexes
if (databaseSchemaVersion < 86 && isBitcoin === true) {
await this.$executeQuery(`
ALTER TABLE \`nodes\`
ADD INDEX \`status\` (\`status\`),
ADD INDEX \`channels\` (\`channels\`),
ADD INDEX \`country_id\` (\`country_id\`),
ADD INDEX \`as_number\` (\`as_number\`),
ADD INDEX \`first_seen\` (\`first_seen\`)
`);
await this.updateToSchemaVersion(86);
}
// lightning node sockets indexes
if (databaseSchemaVersion < 87 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(87);
}
// lightning stats indexes
if (databaseSchemaVersion < 88 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
await this.updateToSchemaVersion(88);
}
// geo names indexes
if (databaseSchemaVersion < 89 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
await this.updateToSchemaVersion(89);
}
// hashrates indexes
if (databaseSchemaVersion < 90 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(90);
}
// block audits indexes
if (databaseSchemaVersion < 91 && isBitcoin === true) {
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
await this.updateToSchemaVersion(91);
}
// elements_pegs indexes
if (databaseSchemaVersion < 92 && config.MEMPOOL.NETWORK === 'liquid') {
await this.$executeQuery(`
ALTER TABLE \`elements_pegs\`
ADD INDEX \`block\` (\`block\`),
ADD INDEX \`datetime\` (\`datetime\`),
ADD INDEX \`amount\` (\`amount\`),
ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
`);
await this.updateToSchemaVersion(92);
}
// federation_txos indexes
if (databaseSchemaVersion < 93 && config.MEMPOOL.NETWORK === 'liquid') {
await this.$executeQuery(`
ALTER TABLE \`federation_txos\`
ADD INDEX \`unspent\` (\`unspent\`),
ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
ADD INDEX \`blocktime\` (\`blocktime\`),
ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
ADD INDEX \`expiredAt\` (\`expiredAt\`)
`);
await this.updateToSchemaVersion(93);
}
// Unify database schema for all mempool netwoks
// versions above 94 should not use network-specific flags
if (databaseSchemaVersion < 94) {
if (!isBitcoin) {
// Apply all the bitcoin specific migrations to non-bitcoin networks: liquid, liquidtestnet and testnet4 (!)
// Version 5
await this.$executeQuery('ALTER TABLE blocks ADD `reward` double unsigned NOT NULL DEFAULT "0"');
// Version 6
await this.$executeQuery('ALTER TABLE blocks MODIFY `height` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `tx_count` smallint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `size` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `weight` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` double NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks DROP FOREIGN KEY IF EXISTS `blocks_ibfk_1`');
await this.$executeQuery('ALTER TABLE pools MODIFY `id` smallint unsigned AUTO_INCREMENT');
await this.$executeQuery('ALTER TABLE blocks MODIFY `pool_id` smallint unsigned NULL');
await this.$executeQuery('ALTER TABLE blocks ADD FOREIGN KEY (`pool_id`) REFERENCES `pools` (`id`)');
await this.$executeQuery('ALTER TABLE blocks ADD `version` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `bits` integer unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `nonce` bigint unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks ADD `merkle_root` varchar(65) NOT NULL DEFAULT ""');
await this.$executeQuery('ALTER TABLE blocks ADD `previous_block_hash` varchar(65) NULL');
// Version 7
await this.$executeQuery('DROP table IF EXISTS hashrates;');
await this.$executeQuery(this.getCreateDailyStatsTableQuery(), await this.$checkIfTableExists('hashrates'));
// Version 8
await this.$executeQuery('ALTER TABLE `hashrates` DROP INDEX `PRIMARY`');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `share` float NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `hashrates` ADD `type` enum("daily", "weekly") DEFAULT "daily"');
// Version 9
await this.$executeQuery('ALTER TABLE `state` CHANGE `name` `name` varchar(100)');
await this.$executeQuery('ALTER TABLE `hashrates` ADD UNIQUE `hashrate_timestamp_pool_id` (`hashrate_timestamp`, `pool_id`)');
// Version 10
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `blockTimestamp` (`blockTimestamp`)');
// Version 11
await this.$executeQuery(`ALTER TABLE blocks
ADD avg_fee INT UNSIGNED NULL,
ADD avg_fee_rate INT UNSIGNED NULL
`);
await this.$executeQuery('ALTER TABLE blocks MODIFY `reward` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` INT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` INT UNSIGNED NOT NULL DEFAULT "0"');
// Version 12
await this.$executeQuery('ALTER TABLE blocks MODIFY `fees` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
// Version 13
await this.$executeQuery('ALTER TABLE blocks MODIFY `difficulty` DOUBLE UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `median_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE blocks MODIFY `avg_fee_rate` BIGINT UNSIGNED NOT NULL DEFAULT "0"');
// Version 14
await this.$executeQuery('ALTER TABLE `hashrates` DROP FOREIGN KEY `hashrates_ibfk_1`');
await this.$executeQuery('ALTER TABLE `hashrates` MODIFY `pool_id` SMALLINT UNSIGNED NOT NULL DEFAULT "0"');
// Version 17
await this.$executeQuery('ALTER TABLE `pools` ADD `slug` CHAR(50) NULL');
// Version 18
await this.$executeQuery('ALTER TABLE `blocks` ADD INDEX `hash` (`hash`);');
// Version 20
await this.$executeQuery(this.getCreateBlocksSummariesTableQuery(), await this.$checkIfTableExists('blocks_summaries'));
// Version 22
await this.$executeQuery('DROP TABLE IF EXISTS `difficulty_adjustments`');
await this.$executeQuery(this.getCreateDifficultyAdjustmentsTableQuery(), await this.$checkIfTableExists('difficulty_adjustments'));
// Version 24
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
// Version 25
await this.$executeQuery(this.getCreateLightningStatisticsQuery(), await this.$checkIfTableExists('lightning_stats'));
await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
// Version 26
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"');
// Version 27
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD avg_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_capacity bigint(20) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_fee_rate int(11) unsigned NOT NULL DEFAULT "0"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD med_base_fee_mtokens bigint(20) unsigned NOT NULL DEFAULT "0"');
// Version 28
await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);
// Version 29
await this.$executeQuery(this.getCreateGeoNamesTableQuery(), await this.$checkIfTableExists('geo_names'));
await this.$executeQuery('ALTER TABLE `nodes` ADD as_number int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD city_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD country_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD accuracy_radius int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD subdivision_id int(11) unsigned NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD longitude double NULL DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD latitude double NULL DEFAULT NULL');
// Version 30
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization") NOT NULL');
// Version 31
await this.$executeQuery('ALTER TABLE `prices` ADD `id` int NULL AUTO_INCREMENT UNIQUE');
await this.$executeQuery('DROP TABLE IF EXISTS `blocks_prices`');
await this.$executeQuery(this.getCreateBlocksPricesTableQuery(), await this.$checkIfTableExists('blocks_prices'));
// Version 32
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD `template` JSON DEFAULT "[]"');
// Version 33
await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL');
// Version 34
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"');
// Version 35
await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"');
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);');
// Version 36
await this.$executeQuery('ALTER TABLE `nodes` ADD status TINYINT NOT NULL DEFAULT "1"');
// Version 37
await this.$executeQuery(this.getCreateLNNodesSocketsTableQuery(), await this.$checkIfTableExists('nodes_sockets'));
// Version 38
await this.$executeQuery(`TRUNCATE lightning_stats`);
await this.$executeQuery(`TRUNCATE node_stats`);
await this.$executeQuery('ALTER TABLE `lightning_stats` CHANGE `added` `added` timestamp NULL');
await this.$executeQuery('ALTER TABLE `node_stats` CHANGE `added` `added` timestamp NULL');
await this.updateToSchemaVersion(38);
// Version 39
await this.$executeQuery('ALTER TABLE `nodes` ADD alias_search TEXT NULL DEFAULT NULL AFTER `alias`');
await this.$executeQuery('ALTER TABLE nodes ADD FULLTEXT(alias_search)');
// Version 40
await this.$executeQuery('ALTER TABLE `nodes` ADD capacity bigint(20) unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD channels int(11) unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `nodes` ADD INDEX `capacity` (`capacity`);');
// Version 41
await this.$executeQuery('UPDATE channels SET closing_reason = NULL WHERE closing_reason = 1');
// Version 42
await this.$executeQuery('ALTER TABLE `channels` ADD closing_resolved tinyint(1) DEFAULT 0');
// Version 43
await this.$executeQuery(this.getCreateLNNodeRecordsTableQuery(), await this.$checkIfTableExists('nodes_records'));
// Version 44
await this.$executeQuery('UPDATE blocks_summaries SET template = NULL');
// Version 45
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fresh_txs JSON DEFAULT "[]"');
// Version 48
await this.$executeQuery('ALTER TABLE `channels` ADD source_checked tinyint(1) DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD closing_fee bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node1_funding_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node2_funding_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node1_closing_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD node2_closing_balance bigint(20) unsigned DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD funding_ratio float unsigned DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `channels` ADD closed_by varchar(66) DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `channels` ADD single_funded tinyint(1) DEFAULT 0');
await this.$executeQuery('ALTER TABLE `channels` ADD outputs JSON DEFAULT "[]"');
// Version 57
await this.$executeQuery(`ALTER TABLE nodes MODIFY updated_at datetime NULL`);
// Version 60
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD sigop_txs JSON DEFAULT "[]"');
// Version 61
if (! await this.$checkIfTableExists('blocks_templates')) {
await this.$executeQuery('CREATE TABLE blocks_templates AS SELECT id, template FROM blocks_summaries WHERE template != "[]"');
}
await this.$executeQuery('ALTER TABLE blocks_templates MODIFY template JSON DEFAULT "[]"');
await this.$executeQuery('ALTER TABLE blocks_templates ADD PRIMARY KEY (id)');
await this.$executeQuery('ALTER TABLE blocks_summaries DROP COLUMN template');
// Version 62
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_fees BIGINT UNSIGNED DEFAULT NULL');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD expected_weight BIGINT UNSIGNED DEFAULT NULL');
// Version 63
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD fullrbf_txs JSON DEFAULT "[]"');
// Version 64
await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
// Version 65
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
// Version 67
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_summaries` ADD INDEX `version` (`version`)');
await this.$executeQuery('ALTER TABLE `blocks_templates` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_templates` ADD INDEX `version` (`version`)');
// Version 76
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD prioritized_txs JSON DEFAULT "[]"');
// Version 81
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD version INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `version` (`version`)');
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD unseen_txs JSON DEFAULT "[]"');
// Version 83
await this.$executeQuery('ALTER TABLE `blocks` ADD first_seen datetime(6) DEFAULT NULL');
// Version 84
await this.$executeQuery(`
ALTER TABLE \`pools\`
ADD INDEX \`slug\` (\`slug\`),
ADD INDEX \`unique_id\` (\`unique_id\`)
`);
// Version 85
await this.$executeQuery(`
ALTER TABLE \`channels\`
ADD INDEX \`created\` (\`created\`),
ADD INDEX \`capacity\` (\`capacity\`),
ADD INDEX \`closing_reason\` (\`closing_reason\`),
ADD INDEX \`closing_resolved\` (\`closing_resolved\`)
`);
// Version 86
await this.$executeQuery(`
ALTER TABLE \`nodes\`
ADD INDEX \`status\` (\`status\`),
ADD INDEX \`channels\` (\`channels\`),
ADD INDEX \`country_id\` (\`country_id\`),
ADD INDEX \`as_number\` (\`as_number\`),
ADD INDEX \`first_seen\` (\`first_seen\`)
`);
// Version 87
await this.$executeQuery('ALTER TABLE `nodes_sockets` ADD INDEX `type` (`type`)');
await this.updateToSchemaVersion(87);
// Version 88
await this.$executeQuery('ALTER TABLE `lightning_stats` ADD INDEX `added` (`added`)');
// Version 89
await this.$executeQuery('ALTER TABLE `geo_names` ADD INDEX `names` (`names`)');
// Version 90
await this.$executeQuery('ALTER TABLE `hashrates` ADD INDEX `type` (`type`)');
// Version 91
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD INDEX `time` (`time`)');
}
if (config.MEMPOOL.NETWORK !== 'liquid') {
// Apply all the liquid specific migrations to all other networks
// Version 68
await this.$executeQuery('ALTER TABLE elements_pegs ADD PRIMARY KEY (txid, txindex);');
await this.$executeQuery(this.getCreateFederationAddressesTableQuery(), await this.$checkIfTableExists('federation_addresses'));
await this.$executeQuery(this.getCreateFederationTxosTableQuery(), await this.$checkIfTableExists('federation_txos'));
// Version 71
await this.$executeQuery('ALTER TABLE `federation_txos` ADD timelock INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `federation_txos` ADD expiredAt INT NOT NULL DEFAULT 0');
await this.$executeQuery('ALTER TABLE `federation_txos` ADD emergencyKey TINYINT NOT NULL DEFAULT 0');
// Version 92
await this.$executeQuery(`
ALTER TABLE \`elements_pegs\`
ADD INDEX \`block\` (\`block\`),
ADD INDEX \`datetime\` (\`datetime\`),
ADD INDEX \`amount\` (\`amount\`),
ADD INDEX \`bitcoinaddress\` (\`bitcoinaddress\`),
ADD INDEX \`bitcointxid\` (\`bitcointxid\`)
`);
// Version 93
await this.$executeQuery(`
ALTER TABLE \`federation_txos\`
ADD INDEX \`unspent\` (\`unspent\`),
ADD INDEX \`lastblockupdate\` (\`lastblockupdate\`),
ADD INDEX \`blocktime\` (\`blocktime\`),
ADD INDEX \`emergencyKey\` (\`emergencyKey\`),
ADD INDEX \`expiredAt\` (\`expiredAt\`)
`);
}
if (config.MEMPOOL.NETWORK !== 'mainnet') {
// Apply all the mainnet specific migrations to all other networks
// Version 69
await this.$executeQuery(this.getCreateAccelerationsTableQuery(), await this.$checkIfTableExists('accelerations'));
// Version 70
await this.$executeQuery('ALTER TABLE accelerations MODIFY COLUMN added DATETIME;');
// Version 77
await this.$executeQuery('ALTER TABLE `accelerations` ADD requested datetime DEFAULT NULL');
}
await this.updateToSchemaVersion(94);
}
} }
/** /**

View file

@ -3,6 +3,8 @@ import { Application, Request, Response } from 'express';
import channelsApi from './channels.api'; import channelsApi from './channels.api';
import { handleError } from '../../utils/api'; import { handleError } from '../../utils/api';
const TXID_REGEX = /^[a-f0-9]{64}$/i;
class ChannelsRoutes { class ChannelsRoutes {
constructor() { } constructor() { }
@ -23,7 +25,7 @@ class ChannelsRoutes {
const channels = await channelsApi.$searchChannelsById(req.params.search); const channels = await channelsApi.$searchChannelsById(req.params.search);
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to search channels by id');
} }
} }
@ -39,7 +41,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channel); res.json(channel);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get channel');
} }
} }
@ -70,7 +72,7 @@ class ChannelsRoutes {
res.header('X-Total-Count', channelsCount.toString()); res.header('X-Total-Count', channelsCount.toString());
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get channels for node');
} }
} }
@ -83,7 +85,10 @@ class ChannelsRoutes {
const txIds: string[] = []; const txIds: string[] = [];
for (const _txId in req.query.txId) { for (const _txId in req.query.txId) {
if (typeof req.query.txId[_txId] === 'string') { if (typeof req.query.txId[_txId] === 'string') {
txIds.push(req.query.txId[_txId].toString()); const txid = req.query.txId[_txId].toString();
if (TXID_REGEX.test(txid)) {
txIds.push(txid);
}
} }
} }
const channels = await channelsApi.$getChannelsByTransactionId(txIds); const channels = await channelsApi.$getChannelsByTransactionId(txIds);
@ -108,7 +113,7 @@ class ChannelsRoutes {
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get channels by transaction ids');
} }
} }
@ -120,7 +125,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get penalty closed channels');
} }
} }
@ -133,7 +138,7 @@ class ChannelsRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get channel geodata');
} }
} }

View file

@ -29,7 +29,7 @@ class GeneralLightningRoutes {
channels: channels, channels: channels,
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to search for nodes and channels');
} }
} }
@ -43,7 +43,7 @@ class GeneralLightningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics); res.json(statistics);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get lightning statistics');
} }
} }
@ -52,7 +52,7 @@ class GeneralLightningRoutes {
const statistics = await statisticsApi.$getLatestStatistics(); const statistics = await statisticsApi.$getLatestStatistics();
res.json(statistics); res.json(statistics);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get lightning statistics');
} }
} }
} }

View file

@ -32,7 +32,7 @@ class NodesRoutes {
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search); const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
res.json(nodes); res.json(nodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to search for node');
} }
} }
@ -188,7 +188,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(nodes); res.json(nodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get node group');
} }
} }
@ -204,7 +204,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node); res.json(node);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get node');
} }
} }
@ -216,7 +216,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(statistics); res.json(statistics);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get historical node stats');
} }
} }
@ -232,7 +232,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(node); res.json(node);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get fee histogram');
} }
} }
@ -248,7 +248,7 @@ class NodesRoutes {
topByChannels: topChannelsNodes, topByChannels: topChannelsNodes,
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get nodes ranking');
} }
} }
@ -260,7 +260,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get top nodes by capacity');
} }
} }
@ -272,7 +272,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get top nodes by channels');
} }
} }
@ -284,7 +284,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(topCapacityNodes); res.json(topCapacityNodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get oldest nodes');
} }
} }
@ -296,7 +296,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs); res.json(nodesPerAs);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get ISP ranking');
} }
} }
@ -308,7 +308,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(worldNodes); res.json(worldNodes);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get world nodes');
} }
} }
@ -336,7 +336,7 @@ class NodesRoutes {
nodes: nodes, nodes: nodes,
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get nodes per country');
} }
} }
@ -363,7 +363,7 @@ class NodesRoutes {
nodes: nodes, nodes: nodes,
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get nodes per ISP');
} }
} }
@ -375,7 +375,7 @@ class NodesRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(nodesPerAs); res.json(nodesPerAs);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get nodes per country');
} }
} }
} }

View file

@ -83,7 +83,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(pegs); res.json(pegs);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get pegs by month');
} }
} }
@ -95,7 +95,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60 * 60).toUTCString());
res.json(reserves); res.json(reserves);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get reserves by month');
} }
} }
@ -107,7 +107,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentSupply); res.json(currentSupply);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get pegs');
} }
} }
@ -119,7 +119,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(currentReserves); res.json(currentReserves);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get reserves');
} }
} }
@ -131,7 +131,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(auditStatus); res.json(auditStatus);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get federation audit status');
} }
} }
@ -143,7 +143,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses); res.json(federationAddresses);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get federation addresses');
} }
} }
@ -155,7 +155,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationAddresses); res.json(federationAddresses);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get federation addresses');
} }
} }
@ -167,7 +167,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos); res.json(federationUtxos);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get federation utxos');
} }
} }
@ -179,7 +179,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(expiredUtxos); res.json(expiredUtxos);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get expired utxos');
} }
} }
@ -191,7 +191,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(federationUtxos); res.json(federationUtxos);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get federation utxos number');
} }
} }
@ -203,7 +203,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos); res.json(emergencySpentUtxos);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get emergency spent utxos');
} }
} }
@ -215,7 +215,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(emergencySpentUtxos); res.json(emergencySpentUtxos);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get emergency spent utxos stats');
} }
} }
@ -227,7 +227,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(recentPegs); res.json(recentPegs);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get pegs list');
} }
} }
@ -239,7 +239,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsVolume); res.json(pegsVolume);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get pegs volume daily');
} }
} }
@ -251,7 +251,7 @@ class LiquidRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(pegsCount); res.json(pegsCount);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get pegs count');
} }
} }

View file

@ -382,7 +382,7 @@ class MempoolBlocks {
const ancestors: Ancestor[] = []; const ancestors: Ancestor[] = [];
const descendants: Ancestor[] = []; const descendants: Ancestor[] = [];
let ancestor: MempoolTransactionExtended let ancestor: MempoolTransactionExtended;
for (const cluster of clusters) { for (const cluster of clusters) {
for (const memberTxid of cluster) { for (const memberTxid of cluster) {
const mempoolTx = mempool[memberTxid]; const mempoolTx = mempool[memberTxid];
@ -462,7 +462,7 @@ class MempoolBlocks {
for (let i = 0; i < block.length; i++) { for (let i = 0; i < block.length; i++) {
const txid = block[i]; const txid = block[i];
if (txid) { if (txid in mempool) {
mempoolTx = mempool[txid]; mempoolTx = mempool[txid];
// save position in projected blocks // save position in projected blocks
mempoolTx.position = { mempoolTx.position = {
@ -481,6 +481,9 @@ class MempoolBlocks {
mempoolTx.acceleratedAt = acceleration?.added; mempoolTx.acceleratedAt = acceleration?.added;
mempoolTx.feeDelta = acceleration?.feeDelta; mempoolTx.feeDelta = acceleration?.feeDelta;
for (const ancestor of mempoolTx.ancestors || []) { for (const ancestor of mempoolTx.ancestors || []) {
if (!(ancestor.txid in mempool)) {
continue;
}
if (!mempool[ancestor.txid].acceleration) { if (!mempool[ancestor.txid].acceleration) {
mempool[ancestor.txid].cpfpDirty = true; mempool[ancestor.txid].cpfpDirty = true;
} }
@ -688,7 +691,7 @@ class MempoolBlocks {
[pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean }; [pool: string]: { name: string, block: number, vsize: number, accelerations: string[], complete: boolean };
} = {}; } = {};
// prepare a list of accelerations in ascending order (we'll pop items off the end of the list) // prepare a list of accelerations in ascending order (we'll pop items off the end of the list)
const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).map(acc => { const accQueue: { acceleration: Acceleration, rate: number, vsize: number }[] = Object.values(accelerations).filter(acc => acc.txid in mempoolCache).map(acc => {
let vsize = mempoolCache[acc.txid].vsize; let vsize = mempoolCache[acc.txid].vsize;
for (const ancestor of mempoolCache[acc.txid].ancestors || []) { for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
vsize += (ancestor.weight / 4); vsize += (ancestor.weight / 4);

View file

@ -10,6 +10,7 @@ import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache'; import rbfCache from './rbf-cache';
import { Acceleration } from './services/acceleration'; import { Acceleration } from './services/acceleration';
import accelerationApi from './services/acceleration';
import redisCache from './redis-cache'; import redisCache from './redis-cache';
import blocks from './blocks'; import blocks from './blocks';
@ -207,7 +208,7 @@ class Mempool {
return txTimes; return txTimes;
} }
public async $updateMempool(transactions: string[], accelerations: Acceleration[] | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> { public async $updateMempool(transactions: string[], accelerations: Record<string, Acceleration> | null, minFeeMempool: string[], minFeeTip: number, pollRate: number): Promise<void> {
logger.debug(`Updating mempool...`); logger.debug(`Updating mempool...`);
// warn if this run stalls the main loop for more than 2 minutes // warn if this run stalls the main loop for more than 2 minutes
@ -354,7 +355,7 @@ class Mempool {
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx)); const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6); this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
const accelerationDelta = accelerations != null ? await this.$updateAccelerations(accelerations) : []; const accelerationDelta = accelerations != null ? await this.updateAccelerations(accelerations) : [];
if (accelerationDelta.length) { if (accelerationDelta.length) {
hasChange = true; hasChange = true;
} }
@ -399,58 +400,11 @@ class Mempool {
return this.accelerations; return this.accelerations;
} }
public $updateAccelerations(newAccelerations: Acceleration[]): string[] { public updateAccelerations(newAccelerationMap: Record<string, Acceleration>): string[] {
try { try {
const changed: string[] = []; const accelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, newAccelerationMap);
const newAccelerationMap: { [txid: string]: Acceleration } = {};
for (const acceleration of newAccelerations) {
// skip transactions we don't know about
if (!this.mempoolCache[acceleration.txid]) {
continue;
}
newAccelerationMap[acceleration.txid] = acceleration;
if (this.accelerations[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else {
if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
} else if (this.accelerations[acceleration.txid].pools?.length) {
let poolsChanged = false;
const pools = new Set();
this.accelerations[acceleration.txid].pools.forEach(pool => {
pools.add(pool);
});
acceleration.pools.forEach(pool => {
if (!pools.has(pool)) {
poolsChanged = true;
} else {
pools.delete(pool);
}
});
if (pools.size > 0) {
poolsChanged = true;
}
if (poolsChanged) {
// pools changed
changed.push(acceleration.txid);
}
}
}
}
for (const oldTxid of Object.keys(this.accelerations)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
this.accelerations = newAccelerationMap; this.accelerations = newAccelerationMap;
return accelerationDelta;
return changed;
} catch (e: any) { } catch (e: any) {
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e)); logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
return []; return [];

View file

@ -72,7 +72,7 @@ class MiningRoutes {
} }
res.status(200).send(response); res.status(200).send(response);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get historical prices');
} }
} }
@ -87,7 +87,7 @@ class MiningRoutes {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message); handleError(req, res, 404, e.message);
} else { } else {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get pool');
} }
} }
} }
@ -106,7 +106,7 @@ class MiningRoutes {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message); handleError(req, res, 404, e.message);
} else { } else {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get blocks for pool');
} }
} }
} }
@ -130,7 +130,7 @@ class MiningRoutes {
res.json(pools); res.json(pools);
} }
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get pools');
} }
} }
@ -144,7 +144,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(stats); res.json(stats);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get pools');
} }
} }
@ -158,7 +158,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(hashrates); res.json(hashrates);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get pools historical hashrate');
} }
} }
@ -175,7 +175,7 @@ class MiningRoutes {
if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) { if (e instanceof Error && e.message.indexOf('This mining pool does not exist') > -1) {
handleError(req, res, 404, e.message); handleError(req, res, 404, e.message);
} else { } else {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get pool historical hashrate');
} }
} }
} }
@ -183,7 +183,7 @@ class MiningRoutes {
private async $getHistoricalHashrate(req: Request, res: Response) { private async $getHistoricalHashrate(req: Request, res: Response) {
let currentHashrate = 0, currentDifficulty = 0; let currentHashrate = 0, currentDifficulty = 0;
try { try {
currentHashrate = await bitcoinClient.getNetworkHashPs(); currentHashrate = await bitcoinClient.getNetworkHashPs(1008);
currentDifficulty = await bitcoinClient.getDifficulty(); currentDifficulty = await bitcoinClient.getDifficulty();
} catch (e) { } catch (e) {
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty'); logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty');
@ -204,7 +204,7 @@ class MiningRoutes {
currentDifficulty: currentDifficulty, currentDifficulty: currentDifficulty,
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get historical hashrate');
} }
} }
@ -218,7 +218,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees); res.json(blockFees);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get historical block fees');
} }
} }
@ -236,7 +236,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFees); res.json(blockFees);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get historical block fees');
} }
} }
@ -250,7 +250,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockRewards); res.json(blockRewards);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get historical block rewards');
} }
} }
@ -264,7 +264,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blockFeeRates); res.json(blockFeeRates);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get historical block fee rates');
} }
} }
@ -282,7 +282,7 @@ class MiningRoutes {
weights: blockWeights weights: blockWeights
}); });
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get historical block size and weight');
} }
} }
@ -294,7 +294,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment])); res.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get historical difficulty adjustments');
} }
} }
@ -304,7 +304,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(response); res.json(response);
} catch (e) { } catch (e) {
res.status(500).end(); handleError(req, res, 500, 'Failed to get reward stats');
} }
} }
@ -318,7 +318,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate])); res.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get historical blocks health');
} }
} }
@ -336,7 +336,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit); res.json(audit);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get block audit');
} }
} }
@ -359,7 +359,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(result); res.json(result);
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get height from timestamp');
} }
} }
@ -372,7 +372,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15)); res.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get block audit scores');
} }
} }
@ -385,7 +385,7 @@ class MiningRoutes {
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
res.json(audit || 'null'); res.json(audit || 'null');
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get block audit score');
} }
} }
@ -400,7 +400,7 @@ class MiningRoutes {
} }
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get accelerations by pool');
} }
} }
@ -416,7 +416,7 @@ class MiningRoutes {
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10); const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get accelerations by height');
} }
} }
@ -431,7 +431,7 @@ class MiningRoutes {
} }
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval)); res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get recent accelerations');
} }
} }
@ -446,7 +446,7 @@ class MiningRoutes {
} }
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval)); res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get acceleration totals');
} }
} }
@ -459,9 +459,9 @@ class MiningRoutes {
handleError(req, res, 400, 'Acceleration data is not available.'); handleError(req, res, 400, 'Acceleration data is not available.');
return; return;
} }
res.status(200).send(accelerationApi.accelerations || []); res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get active accelerations');
} }
} }
@ -473,7 +473,7 @@ class MiningRoutes {
accelerationApi.accelerationRequested(req.params.txid); accelerationApi.accelerationRequested(req.params.txid);
res.status(200).send(); res.status(200).send();
} catch (e) { } catch (e) {
handleError(req, res, 500, e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to request acceleration');
} }
} }
} }

View file

@ -136,9 +136,13 @@ class Mining {
poolsStatistics['blockCount'] = blockCount; poolsStatistics['blockCount'] = blockCount;
const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h'); const totalBlock24h: number = await BlocksRepository.$blockCount(null, '24h');
const totalBlock3d: number = await BlocksRepository.$blockCount(null, '3d');
const totalBlock1w: number = await BlocksRepository.$blockCount(null, '1w');
try { try {
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h); poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d);
poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w);
} catch (e) { } catch (e) {
poolsStatistics['lastEstimatedHashrate'] = 0; poolsStatistics['lastEstimatedHashrate'] = 0;
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining); logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate', logger.tags.mining);

View file

@ -1,10 +1,15 @@
import { Application, Request, Response } from 'express'; import { Application, Request, Response } from 'express';
import config from '../../config'; import config from '../../config';
import pricesUpdater from '../../tasks/price-updater'; import pricesUpdater from '../../tasks/price-updater';
import logger from '../../logger';
import PricesRepository from '../../repositories/PricesRepository';
class PricesRoutes { class PricesRoutes {
public initRoutes(app: Application): void { public initRoutes(app: Application): void {
app.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this)); app
.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/usd-price-history', this.$getAllPrices.bind(this))
;
} }
private $getCurrentPrices(req: Request, res: Response): void { private $getCurrentPrices(req: Request, res: Response): void {
@ -14,6 +19,23 @@ class PricesRoutes {
res.json(pricesUpdater.getLatestPrices()); res.json(pricesUpdater.getLatestPrices());
} }
private async $getAllPrices(req: Request, res: Response): Promise<void> {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR).toUTCString());
try {
const usdPriceHistory = await PricesRepository.$getPricesTimesAndId();
const responseData = usdPriceHistory.map(p => {
return { time: p.time, USD: p.USD };
});
res.status(200).json(responseData);
} catch (e: any) {
logger.err(`Exception ${e} in PricesRoutes::$getAllPrices. Code: ${e.code}. Message: ${e.message}`);
res.status(403).send();
}
}
} }
export default new PricesRoutes(); export default new PricesRoutes();

View file

@ -1,7 +1,10 @@
import { WebSocket } from 'ws';
import config from '../../config'; import config from '../../config';
import logger from '../../logger'; import logger from '../../logger';
import { BlockExtended } from '../../mempool.interfaces'; import { BlockExtended } from '../../mempool.interfaces';
import axios from 'axios'; import axios from 'axios';
import mempool from '../mempool';
import websocketHandler from '../websocket-handler';
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done'; type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
@ -37,14 +40,23 @@ export interface AccelerationHistory {
}; };
class AccelerationApi { class AccelerationApi {
private ws: WebSocket | null = null;
private useWebsocket: boolean = config.MEMPOOL.OFFICIAL && config.MEMPOOL_SERVICES.ACCELERATIONS;
private startedWebsocketLoop: boolean = false;
private websocketConnected: boolean = false;
private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS; private onDemandPollingEnabled = !config.MEMPOOL_SERVICES.ACCELERATIONS;
private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations'); private apiPath = config.MEMPOOL.OFFICIAL ? (config.MEMPOOL_SERVICES.API + '/accelerator/accelerations') : (config.EXTERNAL_DATA_SERVER.MEMPOOL_API + '/accelerations');
private _accelerations: Acceleration[] | null = null; private websocketPath = config.MEMPOOL_SERVICES?.API ? `${config.MEMPOOL_SERVICES.API.replace('https://', 'wss://').replace('http://', 'ws://')}/accelerator/ws` : '/';
private _accelerations: Record<string, Acceleration> = {};
private lastPoll = 0; private lastPoll = 0;
private lastPing = Date.now();
private lastPong = Date.now();
private forcePoll = false; private forcePoll = false;
private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {}; private myAccelerations: Record<string, { status: MyAccelerationStatus, added: number, acceleration?: Acceleration }> = {};
public get accelerations(): Acceleration[] | null { public constructor() {}
public getAccelerations(): Record<string, Acceleration> {
return this._accelerations; return this._accelerations;
} }
@ -72,11 +84,18 @@ class AccelerationApi {
} }
} }
public async $updateAccelerations(): Promise<Acceleration[] | null> { public async $updateAccelerations(): Promise<Record<string, Acceleration> | null> {
if (this.useWebsocket && this.websocketConnected) {
return this._accelerations;
}
if (!this.onDemandPollingEnabled) { if (!this.onDemandPollingEnabled) {
const accelerations = await this.$fetchAccelerations(); const accelerations = await this.$fetchAccelerations();
if (accelerations) { if (accelerations) {
this._accelerations = accelerations; const latestAccelerations = {};
for (const acc of accelerations) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
return this._accelerations; return this._accelerations;
} }
} else { } else {
@ -85,7 +104,7 @@ class AccelerationApi {
return null; return null;
} }
private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> { private async $updateAccelerationsOnDemand(): Promise<Record<string, Acceleration> | null> {
const shouldUpdate = this.forcePoll const shouldUpdate = this.forcePoll
|| this.countMyAccelerationsWithStatus('requested') > 0 || this.countMyAccelerationsWithStatus('requested') > 0
|| (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000))); || (this.countMyAccelerationsWithStatus('accelerating') > 0 && this.lastPoll < (Date.now() - (10 * 60 * 1000)));
@ -120,7 +139,11 @@ class AccelerationApi {
} }
} }
this._accelerations = Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]; const latestAccelerations = {};
for (const acc of Object.values(this.myAccelerations).map(({ acceleration }) => acceleration).filter(acc => acc) as Acceleration[]) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
return this._accelerations; return this._accelerations;
} }
@ -152,6 +175,148 @@ class AccelerationApi {
} }
return anyAccelerated; return anyAccelerated;
} }
// get a list of accelerations that have changed between two sets of accelerations
public getAccelerationDelta(oldAccelerationMap: Record<string, Acceleration>, newAccelerationMap: Record<string, Acceleration>): string[] {
const changed: string[] = [];
const mempoolCache = mempool.getMempool();
for (const acceleration of Object.values(newAccelerationMap)) {
// skip transactions we don't know about
if (!mempoolCache[acceleration.txid]) {
continue;
}
if (oldAccelerationMap[acceleration.txid] == null) {
// new acceleration
changed.push(acceleration.txid);
} else {
if (oldAccelerationMap[acceleration.txid].feeDelta !== acceleration.feeDelta) {
// feeDelta changed
changed.push(acceleration.txid);
} else if (oldAccelerationMap[acceleration.txid].pools?.length) {
let poolsChanged = false;
const pools = new Set();
oldAccelerationMap[acceleration.txid].pools.forEach(pool => {
pools.add(pool);
});
acceleration.pools.forEach(pool => {
if (!pools.has(pool)) {
poolsChanged = true;
} else {
pools.delete(pool);
}
});
if (pools.size > 0) {
poolsChanged = true;
}
if (poolsChanged) {
// pools changed
changed.push(acceleration.txid);
}
}
}
}
for (const oldTxid of Object.keys(oldAccelerationMap)) {
if (!newAccelerationMap[oldTxid]) {
// removed
changed.push(oldTxid);
}
}
return changed;
}
private handleWebsocketMessage(msg: any): void {
if (msg?.accelerations !== null) {
const latestAccelerations = {};
for (const acc of msg?.accelerations || []) {
latestAccelerations[acc.txid] = acc;
}
this._accelerations = latestAccelerations;
websocketHandler.handleAccelerationsChanged(this._accelerations);
}
}
public async connectWebsocket(): Promise<void> {
if (this.startedWebsocketLoop) {
return;
}
while (this.useWebsocket) {
this.startedWebsocketLoop = true;
if (!this.ws) {
this.ws = new WebSocket(this.websocketPath);
this.lastPing = 0;
this.ws.on('open', () => {
logger.info(`Acceleration websocket opened to ${this.websocketPath}`);
this.websocketConnected = true;
this.ws?.send(JSON.stringify({
'watch-accelerations': true
}));
});
this.ws.on('error', (error) => {
let errMsg = `Acceleration websocket error on ${this.websocketPath}: ${error['code']}`;
if (error['errors']) {
errMsg += ' - ' + error['errors'].join(' - ');
}
logger.err(errMsg);
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('close', () => {
logger.info('Acceleration websocket closed');
this.ws = null;
this.websocketConnected = false;
});
this.ws.on('message', (data, isBinary) => {
try {
const msg = (isBinary ? data : data.toString()) as string;
const parsedMsg = msg?.length ? JSON.parse(msg) : null;
this.handleWebsocketMessage(parsedMsg);
} catch (e) {
logger.warn('Failed to parse acceleration websocket message: ' + (e instanceof Error ? e.message : e));
}
});
this.ws.on('ping', () => {
logger.debug('received ping from acceleration websocket server');
});
this.ws.on('pong', () => {
logger.debug('received pong from acceleration websocket server');
this.lastPong = Date.now();
});
} else if (this.websocketConnected) {
if (this.lastPing && this.lastPing > this.lastPong && (Date.now() - this.lastPing > 10000)) {
logger.warn('No pong received within 10 seconds, terminating connection');
try {
this.ws?.terminate();
} catch (e) {
logger.warn('failed to terminate acceleration websocket connection: ' + (e instanceof Error ? e.message : e));
} finally {
this.ws = null;
this.websocketConnected = false;
this.lastPing = 0;
}
} else if (!this.lastPing || (Date.now() - this.lastPing > 30000)) {
logger.debug('sending ping to acceleration websocket server');
if (this.ws?.readyState === WebSocket.OPEN) {
try {
this.ws?.ping();
this.lastPing = Date.now();
} catch (e) {
logger.warn('failed to send ping to acceleration websocket server: ' + (e instanceof Error ? e.message : e));
}
}
}
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
} }
export default new AccelerationApi(); export default new AccelerationApi();

View file

@ -0,0 +1,27 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import WalletApi from './wallets';
import { handleError } from '../../utils/api';
class ServicesRoutes {
public initRoutes(app: Application): void {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'wallet/:walletId', this.$getWallet)
;
}
private async $getWallet(req: Request, res: Response): Promise<void> {
try {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 5).toUTCString());
const walletId = req.params.walletId;
const wallet = await WalletApi.getWallet(walletId);
res.status(200).send(wallet);
} catch (e) {
handleError(req, res, 500, 'Failed to get wallet');
}
}
}
export default new ServicesRoutes();

View file

@ -0,0 +1,153 @@
import config from '../../config';
import logger from '../../logger';
import { IEsploraApi } from '../bitcoin/esplora-api.interface';
import bitcoinApi from '../bitcoin/bitcoin-api-factory';
import axios from 'axios';
import { TransactionExtended } from '../../mempool.interfaces';
interface WalletAddress {
address: string;
active: boolean;
stats: {
funded_txo_count: number;
funded_txo_sum: number;
spent_txo_count: number;
spent_txo_sum: number;
tx_count: number;
};
transactions: IEsploraApi.AddressTxSummary[];
lastSync: number;
}
interface Wallet {
name: string;
addresses: Record<string, WalletAddress>;
lastPoll: number;
}
const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes
class WalletApi {
private wallets: Record<string, Wallet> = {};
private syncing = false;
constructor() {
this.wallets = config.WALLETS.ENABLED ? (config.WALLETS.WALLETS as string[]).reduce((acc, wallet) => {
acc[wallet] = { name: wallet, addresses: {}, lastPoll: 0 };
return acc;
}, {} as Record<string, Wallet>) : {};
}
public getWallet(wallet: string): Record<string, WalletAddress> {
return this.wallets?.[wallet]?.addresses || {};
}
// resync wallet addresses from the services backend
async $syncWallets(): Promise<void> {
if (!config.WALLETS.ENABLED || this.syncing) {
return;
}
this.syncing = true;
for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey];
if (wallet.lastPoll < (Date.now() - POLL_FREQUENCY)) {
try {
const response = await axios.get(config.MEMPOOL_SERVICES.API + `/wallets/${wallet.name}`);
const addresses: Record<string, WalletAddress> = response.data;
const addressList: WalletAddress[] = Object.values(addresses);
// sync all current addresses
for (const address of addressList) {
await this.$syncWalletAddress(wallet, address);
}
// remove old addresses
for (const address of Object.keys(wallet.addresses)) {
if (!addresses[address]) {
delete wallet.addresses[address];
}
}
wallet.lastPoll = Date.now();
logger.debug(`Synced ${Object.keys(wallet.addresses).length} addresses for wallet ${wallet.name}`);
} catch (e) {
logger.err(`Error syncing wallet ${wallet.name}: ${(e instanceof Error ? e.message : e)}`);
}
}
}
this.syncing = false;
}
// resync address transactions from esplora
async $syncWalletAddress(wallet: Wallet, address: WalletAddress): Promise<void> {
// fetch full transaction data if the address is new or still active and hasn't been synced in the last hour
const refreshTransactions = !wallet.addresses[address.address] || (address.active && (Date.now() - wallet.addresses[address.address].lastSync) > 60 * 60 * 1000);
if (refreshTransactions) {
try {
const summary = await bitcoinApi.$getAddressTransactionSummary(address.address);
const addressInfo = await bitcoinApi.$getAddress(address.address);
const walletAddress: WalletAddress = {
address: address.address,
active: address.active,
transactions: summary,
stats: addressInfo.chain_stats,
lastSync: Date.now(),
};
wallet.addresses[address.address] = walletAddress;
} catch (e) {
logger.err(`Error syncing wallet address ${address.address}: ${(e instanceof Error ? e.message : e)}`);
}
}
}
// check a new block for transactions that affect wallet address balances, and add relevant transactions to wallets
processBlock(block: IEsploraApi.Block, blockTxs: TransactionExtended[]): Record<string, IEsploraApi.Transaction[]> {
const walletTransactions: Record<string, IEsploraApi.Transaction[]> = {};
for (const walletKey of Object.keys(this.wallets)) {
const wallet = this.wallets[walletKey];
walletTransactions[walletKey] = [];
for (const tx of blockTxs) {
const funded: Record<string, number> = {};
const spent: Record<string, number> = {};
const fundedCount: Record<string, number> = {};
const spentCount: Record<string, number> = {};
let anyMatch = false;
for (const vin of tx.vin) {
const address = vin.prevout?.scriptpubkey_address;
if (address && wallet.addresses[address]) {
anyMatch = true;
spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0);
spentCount[address] = (spentCount[address] ?? 0) + 1;
}
}
for (const vout of tx.vout) {
const address = vout.scriptpubkey_address;
if (address && wallet.addresses[address]) {
anyMatch = true;
funded[address] = (funded[address] ?? 0) + (vout.value ?? 0);
fundedCount[address] = (fundedCount[address] ?? 0) + 1;
}
}
for (const address of Object.keys({ ...funded, ...spent })) {
// update address stats
wallet.addresses[address].stats.tx_count++;
wallet.addresses[address].stats.funded_txo_count += fundedCount[address] || 0;
wallet.addresses[address].stats.spent_txo_count += spentCount[address] || 0;
wallet.addresses[address].stats.funded_txo_sum += funded[address] || 0;
wallet.addresses[address].stats.spent_txo_sum += spent[address] || 0;
// add tx to summary
const txSummary: IEsploraApi.AddressTxSummary = {
txid: tx.txid,
value: (funded[address] ?? 0) - (spent[address] ?? 0),
height: block.height,
time: block.timestamp,
};
wallet.addresses[address].transactions?.push(txSummary);
}
if (anyMatch) {
walletTransactions[walletKey].push(tx);
}
}
}
return walletTransactions;
}
}
export default new WalletApi();

View file

@ -1,7 +1,7 @@
import { Application, Request, Response } from 'express'; import { Application, Request, Response } from 'express';
import config from '../../config'; import config from '../../config';
import statisticsApi from './statistics-api'; import statisticsApi from './statistics-api';
import { handleError } from '../../utils/api';
class StatisticsRoutes { class StatisticsRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
app app
@ -65,7 +65,7 @@ class StatisticsRoutes {
} }
res.json(result); res.json(result);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); handleError(req, res, 500, 'Failed to get statistics');
} }
} }
} }

View file

@ -16,16 +16,19 @@ import transactionUtils from './transaction-utils';
import rbfCache, { ReplacementInfo } from './rbf-cache'; import rbfCache, { ReplacementInfo } from './rbf-cache';
import difficultyAdjustment from './difficulty-adjustment'; import difficultyAdjustment from './difficulty-adjustment';
import feeApi from './fee-api'; import feeApi from './fee-api';
import BlocksRepository from '../repositories/BlocksRepository';
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 priceUpdater from '../tasks/price-updater'; import priceUpdater from '../tasks/price-updater';
import { ApiPrice } from '../repositories/PricesRepository'; import { ApiPrice } from '../repositories/PricesRepository';
import { Acceleration } from './services/acceleration';
import accelerationApi from './services/acceleration'; import accelerationApi from './services/acceleration';
import mempool from './mempool'; import mempool from './mempool';
import statistics from './statistics/statistics'; import statistics from './statistics/statistics';
import accelerationRepository from '../repositories/AccelerationRepository'; import accelerationRepository from '../repositories/AccelerationRepository';
import bitcoinApi from './bitcoin/bitcoin-api-factory'; import bitcoinApi from './bitcoin/bitcoin-api-factory';
import walletApi from './services/wallets';
interface AddressTransactions { interface AddressTransactions {
mempool: MempoolTransactionExtended[], mempool: MempoolTransactionExtended[],
@ -34,6 +37,7 @@ interface AddressTransactions {
} }
import bitcoinSecondClient from './bitcoin/bitcoin-second-client'; import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import { calculateMempoolTxCpfp } from './cpfp'; import { calculateMempoolTxCpfp } from './cpfp';
import { getRecentFirstSeen } from '../utils/file-read';
// valid 'want' subscriptions // valid 'want' subscriptions
const wantable = [ const wantable = [
@ -57,6 +61,8 @@ class WebsocketHandler {
private lastRbfSummary: ReplacementInfo[] | null = null; private lastRbfSummary: ReplacementInfo[] | null = null;
private mempoolSequence: number = 0; private mempoolSequence: number = 0;
private accelerations: Record<string, Acceleration> = {};
constructor() { } constructor() { }
addWebsocketServer(wss: WebSocket.Server) { addWebsocketServer(wss: WebSocket.Server) {
@ -305,6 +311,14 @@ class WebsocketHandler {
} }
} }
if (parsedMessage && parsedMessage['track-wallet']) {
if (parsedMessage['track-wallet'] === 'stop') {
client['track-wallet'] = null;
} else {
client['track-wallet'] = parsedMessage['track-wallet'];
}
}
if (parsedMessage && parsedMessage['track-asset']) { if (parsedMessage && parsedMessage['track-asset']) {
if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) { if (/^[a-fA-F0-9]{64}$/.test(parsedMessage['track-asset'])) {
client['track-asset'] = parsedMessage['track-asset']; client['track-asset'] = parsedMessage['track-asset'];
@ -484,6 +498,42 @@ class WebsocketHandler {
} }
} }
handleAccelerationsChanged(accelerations: Record<string, Acceleration>): void {
if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server has been set');
}
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
this.accelerations = accelerations;
if (!websocketAccelerationDelta.length) {
return;
}
// pre-compute acceleration delta
const accelerationUpdate = {
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
};
try {
const response = JSON.stringify({
accelerations: accelerationUpdate,
});
for (const server of this.webSocketServers) {
server.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN) {
return;
}
client.send(response);
});
}
} catch (e) {
logger.debug(`Error sending acceleration update to websocket clients: ${e}`);
}
}
handleReorg(): void { handleReorg(): void {
if (!this.webSocketServers.length) { if (!this.webSocketServers.length) {
throw new Error('No WebSocket.Server have been set'); throw new Error('No WebSocket.Server have been set');
@ -560,7 +610,7 @@ class WebsocketHandler {
const vBytesPerSecond = memPool.getVBytesPerSecond(); const vBytesPerSecond = memPool.getVBytesPerSecond();
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat()); const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
const da = difficultyAdjustment.getDifficultyAdjustment(); const da = difficultyAdjustment.getDifficultyAdjustment();
const accelerations = memPool.getAccelerations(); const accelerations = accelerationApi.getAccelerations();
memPool.handleRbfTransactions(rbfTransactions); memPool.handleRbfTransactions(rbfTransactions);
const rbfChanges = rbfCache.getRbfChanges(); const rbfChanges = rbfCache.getRbfChanges();
let rbfReplacements; let rbfReplacements;
@ -668,10 +718,13 @@ class WebsocketHandler {
const addressCache = this.makeAddressCache(newTransactions); const addressCache = this.makeAddressCache(newTransactions);
const removedAddressCache = this.makeAddressCache(deletedTransactions); const removedAddressCache = this.makeAddressCache(deletedTransactions);
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
this.accelerations = accelerations;
// pre-compute acceleration delta // pre-compute acceleration delta
const accelerationUpdate = { const accelerationUpdate = {
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null), added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
removed: accelerationDelta.filter(txid => !accelerations[txid]), removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
}; };
// TODO - Fix indentation after PR is merged // TODO - Fix indentation after PR is merged
@ -1028,6 +1081,14 @@ class WebsocketHandler {
} }
} }
if (config.CORE_RPC.DEBUG_LOG_PATH && block.extras) {
const firstSeen = getRecentFirstSeen(block.id);
if (firstSeen) {
BlocksRepository.$saveFirstSeenTime(block.id, firstSeen);
block.extras.firstSeen = firstSeen;
}
}
const confirmedTxids: { [txid: string]: boolean } = {}; const confirmedTxids: { [txid: string]: boolean } = {};
// Update mempool to remove transactions included in the new block // Update mempool to remove transactions included in the new block
@ -1102,6 +1163,9 @@ class WebsocketHandler {
replaced: replacedTransactions, replaced: replacedTransactions,
}; };
// check for wallet transactions
const walletTransactions = config.WALLETS.ENABLED ? walletApi.processBlock(block, transactions) : [];
const responseCache = { ...this.socketData }; const responseCache = { ...this.socketData };
function getCachedResponse(key, data): string { function getCachedResponse(key, data): string {
if (!responseCache[key]) { if (!responseCache[key]) {
@ -1306,6 +1370,11 @@ class WebsocketHandler {
response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta); response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta);
} }
if (client['track-wallet']) {
const trackedWallet = client['track-wallet'];
response['wallet-transactions'] = getCachedResponse(`wallet-transactions-${trackedWallet}`, walletTransactions[trackedWallet] ?? {});
}
if (Object.keys(response).length) { if (Object.keys(response).length) {
client.send(this.serializeResponse(response)); client.send(this.serializeResponse(response));
} }

View file

@ -86,6 +86,7 @@ interface IConfig {
TIMEOUT: number; TIMEOUT: number;
COOKIE: boolean; COOKIE: boolean;
COOKIE_PATH: string; COOKIE_PATH: string;
DEBUG_LOG_PATH: string;
}; };
SECOND_CORE_RPC: { SECOND_CORE_RPC: {
HOST: string; HOST: string;
@ -161,6 +162,10 @@ interface IConfig {
PAID: boolean; PAID: boolean;
API_KEY: string; API_KEY: string;
}, },
WALLETS: {
ENABLED: boolean;
WALLETS: string[];
}
} }
const defaults: IConfig = { const defaults: IConfig = {
@ -227,7 +232,8 @@ const defaults: IConfig = {
'PASSWORD': 'mempool', 'PASSWORD': 'mempool',
'TIMEOUT': 60000, 'TIMEOUT': 60000,
'COOKIE': false, 'COOKIE': false,
'COOKIE_PATH': '/bitcoin/.cookie' 'COOKIE_PATH': '/bitcoin/.cookie',
'DEBUG_LOG_PATH': '',
}, },
'SECOND_CORE_RPC': { 'SECOND_CORE_RPC': {
'HOST': '127.0.0.1', 'HOST': '127.0.0.1',
@ -322,6 +328,10 @@ const defaults: IConfig = {
'PAID': false, 'PAID': false,
'API_KEY': '', 'API_KEY': '',
}, },
'WALLETS': {
'ENABLED': false,
'WALLETS': [],
},
}; };
class Config implements IConfig { class Config implements IConfig {
@ -343,6 +353,7 @@ class Config implements IConfig {
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES']; MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
REDIS: IConfig['REDIS']; REDIS: IConfig['REDIS'];
FIAT_PRICE: IConfig['FIAT_PRICE']; FIAT_PRICE: IConfig['FIAT_PRICE'];
WALLETS: IConfig['WALLETS'];
constructor() { constructor() {
const configs = this.merge(configFromFile, defaults); const configs = this.merge(configFromFile, defaults);
@ -364,6 +375,7 @@ class Config implements IConfig {
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES; this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
this.REDIS = configs.REDIS; this.REDIS = configs.REDIS;
this.FIAT_PRICE = configs.FIAT_PRICE; this.FIAT_PRICE = configs.FIAT_PRICE;
this.WALLETS = configs.WALLETS;
} }
merge = (...objects: object[]): IConfig => { merge = (...objects: object[]): IConfig => {

View file

@ -32,6 +32,7 @@ import pricesRoutes from './api/prices/prices.routes';
import miningRoutes from './api/mining/mining-routes'; import miningRoutes from './api/mining/mining-routes';
import liquidRoutes from './api/liquid/liquid.routes'; import liquidRoutes from './api/liquid/liquid.routes';
import bitcoinRoutes from './api/bitcoin/bitcoin.routes'; import bitcoinRoutes from './api/bitcoin/bitcoin.routes';
import servicesRoutes from './api/services/services-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'; import priceUpdater from './tasks/price-updater';
@ -46,6 +47,7 @@ import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
import accelerationRoutes from './api/acceleration/acceleration.routes'; import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes'; import aboutRoutes from './api/about.routes';
import mempoolBlocks from './api/mempool-blocks'; import mempoolBlocks from './api/mempool-blocks';
import walletApi from './api/services/wallets';
class Server { class Server {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@ -231,13 +233,17 @@ class Server {
const newMempool = await bitcoinApi.$getRawMempool(); const newMempool = await bitcoinApi.$getRawMempool();
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null; const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1; const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
const newAccelerations = await accelerationApi.$updateAccelerations(); const latestAccelerations = await accelerationApi.$updateAccelerations();
const numHandledBlocks = await blocks.$updateBlocks(); const numHandledBlocks = await blocks.$updateBlocks();
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1); const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
if (numHandledBlocks === 0) { if (numHandledBlocks === 0) {
await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate); await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate);
} }
indexer.$run(); indexer.$run();
if (config.WALLETS.ENABLED) {
// might take a while, so run in the background
walletApi.$syncWallets();
}
if (config.FIAT_PRICE.ENABLED) { if (config.FIAT_PRICE.ENABLED) {
priceUpdater.$run(); priceUpdater.$run();
} }
@ -312,11 +318,15 @@ class Server {
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler)); priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
} }
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler)); loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
accelerationApi.connectWebsocket();
} }
setUpHttpApiRoutes(): void { setUpHttpApiRoutes(): void {
bitcoinRoutes.initRoutes(this.app); bitcoinRoutes.initRoutes(this.app);
bitcoinCoreRoutes.initRoutes(this.app); if (config.MEMPOOL.OFFICIAL) {
bitcoinCoreRoutes.initRoutes(this.app);
}
pricesRoutes.initRoutes(this.app); pricesRoutes.initRoutes(this.app);
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) { if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
statisticsRoutes.initRoutes(this.app); statisticsRoutes.initRoutes(this.app);
@ -335,6 +345,9 @@ class Server {
if (config.MEMPOOL_SERVICES.ACCELERATIONS) { if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
accelerationRoutes.initRoutes(this.app); accelerationRoutes.initRoutes(this.app);
} }
if (config.WALLETS.ENABLED) {
servicesRoutes.initRoutes(this.app);
}
if (!config.MEMPOOL.OFFICIAL) { if (!config.MEMPOOL.OFFICIAL) {
aboutRoutes.initRoutes(this.app); aboutRoutes.initRoutes(this.app);
} }

View file

@ -320,6 +320,7 @@ export interface BlockExtension {
segwitTotalSize: number; segwitTotalSize: number;
segwitTotalWeight: number; segwitTotalWeight: number;
header: string; header: string;
firstSeen: number | null;
utxoSetChange: number; utxoSetChange: number;
// Requires coinstatsindex, will be set to NULL otherwise // Requires coinstatsindex, will be set to NULL otherwise
utxoSetSize: number | null; utxoSetSize: number | null;

View file

@ -57,6 +57,7 @@ interface DatabaseBlock {
utxoSetChange: number; utxoSetChange: number;
utxoSetSize: number; utxoSetSize: number;
totalInputAmt: number; totalInputAmt: number;
firstSeen: number;
} }
const BLOCK_DB_FIELDS = ` const BLOCK_DB_FIELDS = `
@ -99,7 +100,8 @@ const BLOCK_DB_FIELDS = `
blocks.header, blocks.header,
blocks.utxoset_change AS utxoSetChange, blocks.utxoset_change AS utxoSetChange,
blocks.utxoset_size AS utxoSetSize, blocks.utxoset_size AS utxoSetSize,
blocks.total_input_amt AS totalInputAmt blocks.total_input_amt AS totalInputAmt,
UNIX_TIMESTAMP(blocks.first_seen) AS firstSeen
`; `;
class BlocksRepository { class BlocksRepository {
@ -499,7 +501,7 @@ class BlocksRepository {
} }
query += ` ORDER BY height DESC query += ` ORDER BY height DESC
LIMIT 10`; LIMIT 100`;
try { try {
const [rows]: any[] = await DB.query(query, params); const [rows]: any[] = await DB.query(query, params);
@ -1021,6 +1023,24 @@ class BlocksRepository {
} }
} }
/**
* Save block first seen time
*
* @param id
*/
public async $saveFirstSeenTime(id: string, firstSeen: number): Promise<void> {
try {
await DB.query(`
UPDATE blocks SET first_seen = FROM_UNIXTIME(?)
WHERE hash = ?`,
[firstSeen, id]
);
} catch (e) {
logger.err(`Cannot update block first seen time. Reason: ` + (e instanceof Error ? e.message : e));
throw e;
}
}
/** /**
* Convert a mysql row block into a BlockExtended. Note that you * Convert a mysql row block into a BlockExtended. Note that you
* must provide the correct field into dbBlk object param * must provide the correct field into dbBlk object param
@ -1078,6 +1098,7 @@ class BlocksRepository {
extras.utxoSetSize = dbBlk.utxoSetSize; extras.utxoSetSize = dbBlk.utxoSetSize;
extras.totalInputAmt = dbBlk.totalInputAmt; extras.totalInputAmt = dbBlk.totalInputAmt;
extras.virtualSize = dbBlk.weight / 4.0; extras.virtualSize = dbBlk.weight / 4.0;
extras.firstSeen = dbBlk.firstSeen;
// Re-org can happen after indexing so we need to always get the // Re-org can happen after indexing so we need to always get the
// latest state from core // latest state from core

View file

@ -83,6 +83,7 @@ module.exports = {
signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+ signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+
stop: 'stop', stop: 'stop',
submitBlock: 'submitblock', // bitcoind v0.7.0+ submitBlock: 'submitblock', // bitcoind v0.7.0+
submitPackage: 'submitpackage',
validateAddress: 'validateaddress', validateAddress: 'validateaddress',
verifyChain: 'verifychain', // bitcoind v0.9.0+ verifyChain: 'verifychain', // bitcoind v0.9.0+
verifyMessage: 'verifymessage', verifyMessage: 'verifymessage',

View file

@ -0,0 +1,58 @@
import * as fs from 'fs';
import logger from '../logger';
import config from '../config';
function readFile(filePath: string, bufferSize?: number): string[] {
const fileSize = fs.statSync(filePath).size;
const chunkSize = bufferSize || fileSize;
const fileDescriptor = fs.openSync(filePath, 'r');
const buffer = Buffer.alloc(chunkSize);
fs.readSync(fileDescriptor, buffer, 0, chunkSize, fileSize - chunkSize);
fs.closeSync(fileDescriptor);
const lines = buffer.toString('utf8', 0, chunkSize).split('\n');
return lines;
}
function extractDateFromLogLine(line: string): number | undefined {
// Extract time from log: "2021-08-31T12:34:56Z" or "2021-08-31T12:34:56.123456Z"
const dateMatch = line.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{6})?Z/);
if (!dateMatch) {
return undefined;
}
const dateStr = dateMatch[0];
const date = new Date(dateStr);
let timestamp = Math.floor(date.getTime() / 1000); // Remove decimal (microseconds are added later)
const timePart = dateStr.split('T')[1];
const microseconds = timePart.split('.')[1] || '';
if (!microseconds) {
return timestamp;
}
return parseFloat(timestamp + '.' + microseconds);
}
export function getRecentFirstSeen(hash: string): number | undefined {
const debugLogPath = config.CORE_RPC.DEBUG_LOG_PATH;
if (debugLogPath) {
try {
// Read the last few lines of debug.log
const lines = readFile(debugLogPath, 2048);
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i];
if (line && line.includes(`Saw new header hash=${hash}`)) {
return extractDateFromLogLine(line);
}
}
} catch (e) {
logger.err(`Cannot parse block first seen time from Core logs. Reason: ` + (e instanceof Error ? e.message : e));
}
}
return undefined;
}

View file

@ -47,7 +47,8 @@
"PASSWORD": "__CORE_RPC_PASSWORD__", "PASSWORD": "__CORE_RPC_PASSWORD__",
"TIMEOUT": __CORE_RPC_TIMEOUT__, "TIMEOUT": __CORE_RPC_TIMEOUT__,
"COOKIE": __CORE_RPC_COOKIE__, "COOKIE": __CORE_RPC_COOKIE__,
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__" "COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__",
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
}, },
"ELECTRUM": { "ELECTRUM": {
"HOST": "__ELECTRUM_HOST__", "HOST": "__ELECTRUM_HOST__",

View file

@ -49,6 +49,7 @@ __CORE_RPC_PASSWORD__=${CORE_RPC_PASSWORD:=mempool}
__CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000} __CORE_RPC_TIMEOUT__=${CORE_RPC_TIMEOUT:=60000}
__CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false} __CORE_RPC_COOKIE__=${CORE_RPC_COOKIE:=false}
__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""} __CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""}
__CORE_RPC_DEBUG_LOG_PATH__=${CORE_RPC_DEBUG_LOG_PATH:=""}
# ELECTRUM # ELECTRUM
__ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1} __ELECTRUM_HOST__=${ELECTRUM_HOST:=127.0.0.1}
@ -207,6 +208,7 @@ sed -i "s!__CORE_RPC_PASSWORD__!${__CORE_RPC_PASSWORD__}!g" mempool-config.json
sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json sed -i "s!__CORE_RPC_TIMEOUT__!${__CORE_RPC_TIMEOUT__}!g" mempool-config.json
sed -i "s!__CORE_RPC_COOKIE__!${__CORE_RPC_COOKIE__}!g" mempool-config.json sed -i "s!__CORE_RPC_COOKIE__!${__CORE_RPC_COOKIE__}!g" mempool-config.json
sed -i "s!__CORE_RPC_COOKIE_PATH__!${__CORE_RPC_COOKIE_PATH__}!g" mempool-config.json sed -i "s!__CORE_RPC_COOKIE_PATH__!${__CORE_RPC_COOKIE_PATH__}!g" mempool-config.json
sed -i "s!__CORE_RPC_DEBUG_LOG_PATH__!${__CORE_RPC_DEBUG_LOG_PATH__}!g" mempool-config.json
sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json sed -i "s!__ELECTRUM_HOST__!${__ELECTRUM_HOST__}!g" mempool-config.json
sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json sed -i "s!__ELECTRUM_PORT__!${__ELECTRUM_PORT__}!g" mempool-config.json

View file

@ -0,0 +1,48 @@
{
"theme": "wiz",
"enterprise": "bitb",
"branding": {
"name": "bitb",
"title": "BITB",
"site_id": 20,
"header_img": "/resources/bitblogo.svg",
"footer_img": "/resources/bitblogo.svg"
},
"dashboard": {
"widgets": [
{
"component": "fees",
"mobileOrder": 4
},
{
"component": "walletBalance",
"mobileOrder": 1,
"props": {
"wallet": "BITB"
}
},
{
"component": "goggles",
"mobileOrder": 5
},
{
"component": "wallet",
"mobileOrder": 2,
"props": {
"wallet": "BITB",
"period": "all"
}
},
{
"component": "blocks"
},
{
"component": "walletTransactions",
"mobileOrder": 3,
"props": {
"wallet": "BITB"
}
}
]
}
}

View file

@ -0,0 +1,51 @@
{
"theme": "contrast",
"enterprise": "meta",
"branding": {
"name": "metaplanet",
"title": "Metaplanet",
"site_id": 21,
"header_img": "/resources/metalogo.svg",
"footer_img": "/resources/metalogo.svg"
},
"dashboard": {
"widgets": [
{
"component": "fees",
"mobileOrder": 4
},
{
"component": "walletBalance",
"mobileOrder": 1,
"props": {
"wallet": "3350"
}
},
{
"component": "twitter",
"mobileOrder": 5,
"props": {
"handle": "Metaplanet_JP"
}
},
{
"component": "wallet",
"mobileOrder": 2,
"props": {
"wallet": "3350",
"period": "all"
}
},
{
"component": "blocks"
},
{
"component": "walletTransactions",
"mobileOrder": 3,
"props": {
"wallet": "3350"
}
}
]
}
}

View file

@ -42,7 +42,7 @@
"rxjs": "~7.8.1", "rxjs": "~7.8.1",
"tinyify": "^4.0.0", "tinyify": "^4.0.0",
"tlite": "^0.1.9", "tlite": "^0.1.9",
"tslib": "~2.7.0", "tslib": "~2.8.0",
"zone.js": "~0.14.4" "zone.js": "~0.14.4"
}, },
"devDependencies": { "devDependencies": {
@ -51,7 +51,7 @@
"@types/node": "^18.11.9", "@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0", "@typescript-eslint/parser": "^7.4.0",
"browser-sync": "^3.0.0", "browser-sync": "^3.0.3",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"http-proxy-middleware": "~2.0.6", "http-proxy-middleware": "~2.0.6",
"prettier": "^3.0.0", "prettier": "^3.0.0",
@ -4800,9 +4800,9 @@
"devOptional": true "devOptional": true
}, },
"node_modules/@types/cors": { "node_modules/@types/cors": {
"version": "2.8.13", "version": "2.8.17",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
"integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
@ -6209,13 +6209,13 @@
} }
}, },
"node_modules/browser-sync": { "node_modules/browser-sync": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.2.tgz", "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.3.tgz",
"integrity": "sha512-PC9c7aWJFVR4IFySrJxOqLwB9ENn3/TaXCXtAa0SzLwocLN3qMjN+IatbjvtCX92BjNXsY6YWg9Eb7F3Wy255g==", "integrity": "sha512-91hoBHKk1C4pGeD+oE9Ld222k2GNQEAsI5AElqR8iLLWNrmZR2LPP8B0h8dpld9u7kro5IEUB3pUb0DJ3n1cRQ==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"browser-sync-client": "^3.0.2", "browser-sync-client": "^3.0.3",
"browser-sync-ui": "^3.0.2", "browser-sync-ui": "^3.0.3",
"bs-recipes": "1.3.4", "bs-recipes": "1.3.4",
"chalk": "4.1.2", "chalk": "4.1.2",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
@ -6229,15 +6229,15 @@
"fs-extra": "3.0.1", "fs-extra": "3.0.1",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"immutable": "^3", "immutable": "^3",
"micromatch": "^4.0.2", "micromatch": "^4.0.8",
"opn": "5.3.0", "opn": "5.3.0",
"portscanner": "2.2.0", "portscanner": "2.2.0",
"raw-body": "^2.3.2", "raw-body": "^2.3.2",
"resp-modifier": "6.0.2", "resp-modifier": "6.0.2",
"rx": "4.1.0", "rx": "4.1.0",
"send": "0.16.2", "send": "^0.19.0",
"serve-index": "1.9.1", "serve-index": "^1.9.1",
"serve-static": "1.13.2", "serve-static": "^1.16.2",
"server-destroy": "1.0.1", "server-destroy": "1.0.1",
"socket.io": "^4.4.1", "socket.io": "^4.4.1",
"ua-parser-js": "^1.0.33", "ua-parser-js": "^1.0.33",
@ -6251,9 +6251,9 @@
} }
}, },
"node_modules/browser-sync-client": { "node_modules/browser-sync-client": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.2.tgz", "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.3.tgz",
"integrity": "sha512-tBWdfn9L0wd2Pjuz/NWHtNEKthVb1Y67vg8/qyGNtCqetNz5lkDkFnrsx5UhPNPYUO8vci50IWC/BhYaQskDiQ==", "integrity": "sha512-TOEXaMgYNjBYIcmX5zDlOdjEqCeCN/d7opf/fuyUD/hhGVCfP54iQIDhENCi012AqzYZm3BvuFl57vbwSTwkSQ==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"etag": "1.8.1", "etag": "1.8.1",
@ -6265,9 +6265,9 @@
} }
}, },
"node_modules/browser-sync-ui": { "node_modules/browser-sync-ui": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.2.tgz", "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.3.tgz",
"integrity": "sha512-V3FwWAI+abVbFLTyJjXJlCMBwjc3GXf/BPGfwO2fMFACWbIGW9/4SrBOFYEOOtqzCjQE0Di+U3VIb7eES4omNA==", "integrity": "sha512-FcGWo5lP5VodPY6O/f4pXQy5FFh4JK0f2/fTBsp0Lx1NtyBWs/IfPPJbW8m1ujTW/2r07oUXKTF2LYZlCZktjw==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"async-each-series": "0.1.1", "async-each-series": "0.1.1",
@ -6412,30 +6412,6 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"devOptional": true "devOptional": true
}, },
"node_modules/browser-sync/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"devOptional": true,
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/browser-sync/node_modules/depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
"devOptional": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/browser-sync/node_modules/destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==",
"devOptional": true
},
"node_modules/browser-sync/node_modules/fs-extra": { "node_modules/browser-sync/node_modules/fs-extra": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz",
@ -6456,27 +6432,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/browser-sync/node_modules/http-errors": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==",
"devOptional": true,
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.0",
"statuses": ">= 1.4.0 < 2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/browser-sync/node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
"devOptional": true
},
"node_modules/browser-sync/node_modules/jsonfile": { "node_modules/browser-sync/node_modules/jsonfile": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz",
@ -6486,75 +6441,6 @@
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
}, },
"node_modules/browser-sync/node_modules/mime": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
"integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==",
"devOptional": true,
"bin": {
"mime": "cli.js"
}
},
"node_modules/browser-sync/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"devOptional": true
},
"node_modules/browser-sync/node_modules/send": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
"integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
"devOptional": true,
"dependencies": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.6.2",
"mime": "1.4.1",
"ms": "2.0.0",
"on-finished": "~2.3.0",
"range-parser": "~1.2.0",
"statuses": "~1.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/browser-sync/node_modules/serve-static": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
"integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
"devOptional": true,
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.2",
"send": "0.16.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/browser-sync/node_modules/setprototypeof": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
"devOptional": true
},
"node_modules/browser-sync/node_modules/statuses": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==",
"devOptional": true,
"engines": {
"node": ">= 0.6"
}
},
"node_modules/browser-sync/node_modules/supports-color": { "node_modules/browser-sync/node_modules/supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -7695,9 +7581,9 @@
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
}, },
"node_modules/cookie": { "node_modules/cookie": {
"version": "0.6.0", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
} }
@ -8906,9 +8792,9 @@
} }
}, },
"node_modules/engine.io": { "node_modules/engine.io": {
"version": "6.5.5", "version": "6.6.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
"integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"@types/cookie": "^0.4.1", "@types/cookie": "^0.4.1",
@ -8916,7 +8802,7 @@
"@types/node": ">=10.0.0", "@types/node": ">=10.0.0",
"accepts": "~1.3.4", "accepts": "~1.3.4",
"base64id": "2.0.0", "base64id": "2.0.0",
"cookie": "~0.4.1", "cookie": "~0.7.2",
"cors": "~2.8.5", "cors": "~2.8.5",
"debug": "~4.3.1", "debug": "~4.3.1",
"engine.io-parser": "~5.2.1", "engine.io-parser": "~5.2.1",
@ -8927,16 +8813,16 @@
} }
}, },
"node_modules/engine.io-client": { "node_modules/engine.io-client": {
"version": "6.5.4", "version": "6.6.1",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz",
"integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"@socket.io/component-emitter": "~3.1.0", "@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1", "debug": "~4.3.1",
"engine.io-parser": "~5.2.1", "engine.io-parser": "~5.2.1",
"ws": "~8.17.1", "ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.0.0" "xmlhttprequest-ssl": "~2.1.1"
} }
}, },
"node_modules/engine.io-parser": { "node_modules/engine.io-parser": {
@ -8949,9 +8835,9 @@
} }
}, },
"node_modules/engine.io/node_modules/cookie": { "node_modules/engine.io/node_modules/cookie": {
"version": "0.4.2", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"devOptional": true, "devOptional": true,
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@ -9846,16 +9732,16 @@
"integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw=="
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.21.0", "version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.7.1",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@ -15893,21 +15779,21 @@
} }
}, },
"node_modules/socket.io": { "node_modules/socket.io": {
"version": "4.7.1", "version": "4.8.0",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.1.tgz", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz",
"integrity": "sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw==", "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"accepts": "~1.3.4", "accepts": "~1.3.4",
"base64id": "~2.0.0", "base64id": "~2.0.0",
"cors": "~2.8.5", "cors": "~2.8.5",
"debug": "~4.3.2", "debug": "~4.3.2",
"engine.io": "~6.5.0", "engine.io": "~6.6.0",
"socket.io-adapter": "~2.5.2", "socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4" "socket.io-parser": "~4.2.4"
}, },
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.2.0"
} }
}, },
"node_modules/socket.io-adapter": { "node_modules/socket.io-adapter": {
@ -15921,14 +15807,14 @@
} }
}, },
"node_modules/socket.io-client": { "node_modules/socket.io-client": {
"version": "4.7.5", "version": "4.8.0",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz",
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", "integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"@socket.io/component-emitter": "~3.1.0", "@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2", "debug": "~4.3.2",
"engine.io-client": "~6.5.2", "engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4" "socket.io-parser": "~4.2.4"
}, },
"engines": { "engines": {
@ -16903,9 +16789,9 @@
} }
}, },
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.7.0", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="
}, },
"node_modules/tuf-js": { "node_modules/tuf-js": {
"version": "2.2.0", "version": "2.2.0",
@ -18290,9 +18176,9 @@
} }
}, },
"node_modules/xmlhttprequest-ssl": { "node_modules/xmlhttprequest-ssl": {
"version": "2.0.0", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==",
"devOptional": true, "devOptional": true,
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
@ -21585,9 +21471,9 @@
"devOptional": true "devOptional": true
}, },
"@types/cors": { "@types/cors": {
"version": "2.8.13", "version": "2.8.17",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
"integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
"devOptional": true, "devOptional": true,
"requires": { "requires": {
"@types/node": "*" "@types/node": "*"
@ -22686,13 +22572,13 @@
} }
}, },
"browser-sync": { "browser-sync": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.2.tgz", "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.3.tgz",
"integrity": "sha512-PC9c7aWJFVR4IFySrJxOqLwB9ENn3/TaXCXtAa0SzLwocLN3qMjN+IatbjvtCX92BjNXsY6YWg9Eb7F3Wy255g==", "integrity": "sha512-91hoBHKk1C4pGeD+oE9Ld222k2GNQEAsI5AElqR8iLLWNrmZR2LPP8B0h8dpld9u7kro5IEUB3pUb0DJ3n1cRQ==",
"devOptional": true, "devOptional": true,
"requires": { "requires": {
"browser-sync-client": "^3.0.2", "browser-sync-client": "^3.0.3",
"browser-sync-ui": "^3.0.2", "browser-sync-ui": "^3.0.3",
"bs-recipes": "1.3.4", "bs-recipes": "1.3.4",
"chalk": "4.1.2", "chalk": "4.1.2",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
@ -22706,15 +22592,15 @@
"fs-extra": "3.0.1", "fs-extra": "3.0.1",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"immutable": "^3", "immutable": "^3",
"micromatch": "^4.0.2", "micromatch": "^4.0.8",
"opn": "5.3.0", "opn": "5.3.0",
"portscanner": "2.2.0", "portscanner": "2.2.0",
"raw-body": "^2.3.2", "raw-body": "^2.3.2",
"resp-modifier": "6.0.2", "resp-modifier": "6.0.2",
"rx": "4.1.0", "rx": "4.1.0",
"send": "0.16.2", "send": "^0.19.0",
"serve-index": "1.9.1", "serve-index": "^1.9.1",
"serve-static": "1.13.2", "serve-static": "^1.16.2",
"server-destroy": "1.0.1", "server-destroy": "1.0.1",
"socket.io": "^4.4.1", "socket.io": "^4.4.1",
"ua-parser-js": "^1.0.33", "ua-parser-js": "^1.0.33",
@ -22766,27 +22652,6 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"devOptional": true "devOptional": true
}, },
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"devOptional": true,
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
"devOptional": true
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==",
"devOptional": true
},
"fs-extra": { "fs-extra": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz",
@ -22804,24 +22669,6 @@
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"devOptional": true "devOptional": true
}, },
"http-errors": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==",
"devOptional": true,
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.0",
"statuses": ">= 1.4.0 < 2"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
"devOptional": true
},
"jsonfile": { "jsonfile": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz",
@ -22831,63 +22678,6 @@
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
}, },
"mime": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
"integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==",
"devOptional": true
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"devOptional": true
},
"send": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
"integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
"devOptional": true,
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.6.2",
"mime": "1.4.1",
"ms": "2.0.0",
"on-finished": "~2.3.0",
"range-parser": "~1.2.0",
"statuses": "~1.4.0"
}
},
"serve-static": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
"integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
"devOptional": true,
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.2",
"send": "0.16.2"
}
},
"setprototypeof": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==",
"devOptional": true
},
"statuses": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==",
"devOptional": true
},
"supports-color": { "supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@ -22950,9 +22740,9 @@
} }
}, },
"browser-sync-client": { "browser-sync-client": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.2.tgz", "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.3.tgz",
"integrity": "sha512-tBWdfn9L0wd2Pjuz/NWHtNEKthVb1Y67vg8/qyGNtCqetNz5lkDkFnrsx5UhPNPYUO8vci50IWC/BhYaQskDiQ==", "integrity": "sha512-TOEXaMgYNjBYIcmX5zDlOdjEqCeCN/d7opf/fuyUD/hhGVCfP54iQIDhENCi012AqzYZm3BvuFl57vbwSTwkSQ==",
"devOptional": true, "devOptional": true,
"requires": { "requires": {
"etag": "1.8.1", "etag": "1.8.1",
@ -22961,9 +22751,9 @@
} }
}, },
"browser-sync-ui": { "browser-sync-ui": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.2.tgz", "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.3.tgz",
"integrity": "sha512-V3FwWAI+abVbFLTyJjXJlCMBwjc3GXf/BPGfwO2fMFACWbIGW9/4SrBOFYEOOtqzCjQE0Di+U3VIb7eES4omNA==", "integrity": "sha512-FcGWo5lP5VodPY6O/f4pXQy5FFh4JK0f2/fTBsp0Lx1NtyBWs/IfPPJbW8m1ujTW/2r07oUXKTF2LYZlCZktjw==",
"devOptional": true, "devOptional": true,
"requires": { "requires": {
"async-each-series": "0.1.1", "async-each-series": "0.1.1",
@ -23833,9 +23623,9 @@
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
}, },
"cookie": { "cookie": {
"version": "0.6.0", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="
}, },
"cookie-signature": { "cookie-signature": {
"version": "1.0.6", "version": "1.0.6",
@ -24771,9 +24561,9 @@
} }
}, },
"engine.io": { "engine.io": {
"version": "6.5.5", "version": "6.6.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
"integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==",
"devOptional": true, "devOptional": true,
"requires": { "requires": {
"@types/cookie": "^0.4.1", "@types/cookie": "^0.4.1",
@ -24781,7 +24571,7 @@
"@types/node": ">=10.0.0", "@types/node": ">=10.0.0",
"accepts": "~1.3.4", "accepts": "~1.3.4",
"base64id": "2.0.0", "base64id": "2.0.0",
"cookie": "~0.4.1", "cookie": "~0.7.2",
"cors": "~2.8.5", "cors": "~2.8.5",
"debug": "~4.3.1", "debug": "~4.3.1",
"engine.io-parser": "~5.2.1", "engine.io-parser": "~5.2.1",
@ -24789,24 +24579,24 @@
}, },
"dependencies": { "dependencies": {
"cookie": { "cookie": {
"version": "0.4.2", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"devOptional": true "devOptional": true
} }
} }
}, },
"engine.io-client": { "engine.io-client": {
"version": "6.5.4", "version": "6.6.1",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz",
"integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==",
"devOptional": true, "devOptional": true,
"requires": { "requires": {
"@socket.io/component-emitter": "~3.1.0", "@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1", "debug": "~4.3.1",
"engine.io-parser": "~5.2.1", "engine.io-parser": "~5.2.1",
"ws": "~8.17.1", "ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.0.0" "xmlhttprequest-ssl": "~2.1.1"
} }
}, },
"engine.io-parser": { "engine.io-parser": {
@ -25497,16 +25287,16 @@
"integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw=="
}, },
"express": { "express": {
"version": "4.21.0", "version": "4.21.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
"requires": { "requires": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.7.1",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@ -29962,16 +29752,16 @@
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="
}, },
"socket.io": { "socket.io": {
"version": "4.7.1", "version": "4.8.0",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.1.tgz", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz",
"integrity": "sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw==", "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==",
"devOptional": true, "devOptional": true,
"requires": { "requires": {
"accepts": "~1.3.4", "accepts": "~1.3.4",
"base64id": "~2.0.0", "base64id": "~2.0.0",
"cors": "~2.8.5", "cors": "~2.8.5",
"debug": "~4.3.2", "debug": "~4.3.2",
"engine.io": "~6.5.0", "engine.io": "~6.6.0",
"socket.io-adapter": "~2.5.2", "socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4" "socket.io-parser": "~4.2.4"
} }
@ -29987,14 +29777,14 @@
} }
}, },
"socket.io-client": { "socket.io-client": {
"version": "4.7.5", "version": "4.8.0",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz",
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", "integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==",
"devOptional": true, "devOptional": true,
"requires": { "requires": {
"@socket.io/component-emitter": "~3.1.0", "@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2", "debug": "~4.3.2",
"engine.io-client": "~6.5.2", "engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4" "socket.io-parser": "~4.2.4"
} }
}, },
@ -30724,9 +30514,9 @@
} }
}, },
"tslib": { "tslib": {
"version": "2.7.0", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="
}, },
"tuf-js": { "tuf-js": {
"version": "2.2.0", "version": "2.2.0",
@ -31573,9 +31363,9 @@
"integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==" "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw=="
}, },
"xmlhttprequest-ssl": { "xmlhttprequest-ssl": {
"version": "2.0.0", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==",
"devOptional": true "devOptional": true
}, },
"xtend": { "xtend": {

View file

@ -95,7 +95,7 @@
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
"tinyify": "^4.0.0", "tinyify": "^4.0.0",
"tlite": "^0.1.9", "tlite": "^0.1.9",
"tslib": "~2.7.0", "tslib": "~2.8.0",
"zone.js": "~0.14.4" "zone.js": "~0.14.4"
}, },
"devDependencies": { "devDependencies": {
@ -105,7 +105,7 @@
"@typescript-eslint/eslint-plugin": "^7.4.0", "@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0", "@typescript-eslint/parser": "^7.4.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"browser-sync": "^3.0.0", "browser-sync": "^3.0.3",
"http-proxy-middleware": "~2.0.6", "http-proxy-middleware": "~2.0.6",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",

View file

@ -1,15 +1,15 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { AppPreloadingStrategy } from './app.preloading-strategy' import { AppPreloadingStrategy } from '@app/app.preloading-strategy'
import { BlockViewComponent } from './components/block-view/block-view.component'; import { BlockViewComponent } from '@components/block-view/block-view.component';
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component'; import { EightBlocksComponent } from '@components/eight-blocks/eight-blocks.component';
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component'; import { MempoolBlockViewComponent } from '@components/mempool-block-view/mempool-block-view.component';
import { ClockComponent } from './components/clock/clock.component'; import { ClockComponent } from '@components/clock/clock.component';
import { StatusViewComponent } from './components/status-view/status-view.component'; import { StatusViewComponent } from '@components/status-view/status-view.component';
import { AddressGroupComponent } from './components/address-group/address-group.component'; import { AddressGroupComponent } from '@components/address-group/address-group.component';
import { TrackerComponent } from './components/tracker/tracker.component'; import { TrackerComponent } from '@components/tracker/tracker.component';
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component'; import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component';
import { TrackerGuard } from './route-guards'; import { TrackerGuard } from '@app/route-guards';
const browserWindow = window || {}; const browserWindow = window || {};
// @ts-ignore // @ts-ignore
@ -22,16 +22,16 @@ let routes: Routes = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'wallet', path: 'widget/wallet',
children: [], children: [],
component: AddressGroupComponent, component: AddressGroupComponent,
data: { data: {
@ -45,7 +45,7 @@ let routes: Routes = [
}, },
{ {
path: '', path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
@ -60,12 +60,12 @@ let routes: Routes = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
@ -83,7 +83,7 @@ let routes: Routes = [
}, },
{ {
path: '', path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
@ -103,16 +103,16 @@ let routes: Routes = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'wallet', path: 'widget/wallet',
children: [], children: [],
component: AddressGroupComponent, component: AddressGroupComponent,
data: { data: {
@ -126,7 +126,7 @@ let routes: Routes = [
}, },
{ {
path: '', path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
@ -138,22 +138,22 @@ let routes: Routes = [
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'tx', path: 'tx',
canMatch: [TrackerGuard], canMatch: [TrackerGuard],
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule), loadChildren: () => import('@components/tracker/tracker.module').then(m => m.TrackerModule),
}, },
{ {
path: '', path: '',
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'wallet', path: 'widget/wallet',
children: [], children: [],
component: AddressGroupComponent, component: AddressGroupComponent,
data: { data: {
@ -165,19 +165,19 @@ let routes: Routes = [
children: [ children: [
{ {
path: '', path: '',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
}, },
{ {
path: 'testnet', path: 'testnet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
}, },
{ {
path: 'testnet4', path: 'testnet4',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
}, },
{ {
path: 'signet', path: 'signet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
}, },
], ],
}, },
@ -212,7 +212,7 @@ let routes: Routes = [
}, },
{ {
path: '', path: '',
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
]; ];
@ -225,16 +225,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'wallet', path: 'widget/wallet',
children: [], children: [],
component: AddressGroupComponent, component: AddressGroupComponent,
data: { data: {
@ -248,7 +248,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
}, },
{ {
path: '', path: '',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
@ -260,16 +260,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{ {
path: '', path: '',
pathMatch: 'full', pathMatch: 'full',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: '', path: '',
loadChildren: () => import ('./liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule), loadChildren: () => import ('@app/liquid/liquid-master-page.module').then(m => m.LiquidMasterPageModule),
data: { preload: true }, data: { preload: true },
}, },
{ {
path: 'wallet', path: 'widget/wallet',
children: [], children: [],
component: AddressGroupComponent, component: AddressGroupComponent,
data: { data: {
@ -281,11 +281,11 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
children: [ children: [
{ {
path: '', path: '',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
}, },
{ {
path: 'testnet', path: 'testnet',
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule) loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
}, },
], ],
}, },
@ -296,7 +296,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
}, },
{ {
path: '', path: '',
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule), loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
data: { preload: true }, data: { preload: true },
}, },
]; ];

View file

@ -440,3 +440,38 @@ export const fiatCurrencies = {
indexed: true, indexed: true,
}, },
}; };
export interface Timezone {
offset: string;
name: string;
}
export const timezones: Timezone[] = [
{ offset: '-12', name: 'Anywhere on Earth (AoE)' },
{ offset: '-11', name: 'Samoa Standard Time (SST)' },
{ offset: '-10', name: 'Hawaii Standard Time (HST)' },
{ offset: '-9', name: 'Alaska Standard Time (AKST)' },
{ offset: '-8', name: 'Pacific Standard Time (PST)' },
{ offset: '-7', name: 'Mountain Standard Time (MST)' },
{ offset: '-6', name: 'Central Standard Time (CST)' },
{ offset: '-5', name: 'Eastern Standard Time (EST)' },
{ offset: '-4', name: 'Atlantic Standard Time (AST)' },
{ offset: '-3', name: 'Argentina Time (ART)' },
{ offset: '-2', name: 'Fernando de Noronha Time (FNT)' },
{ offset: '-1', name: 'Azores Time (AZOT)' },
{ offset: '+0', name: 'Greenwich Mean Time (GMT)' },
{ offset: '+1', name: 'Central European Time (CET)' },
{ offset: '+2', name: 'Eastern European Time (EET)' },
{ offset: '+3', name: 'Moscow Standard Time (MSK)' },
{ offset: '+4', name: 'Armenia Time (AMT)' },
{ offset: '+5', name: 'Pakistan Standard Time (PKT)' },
{ offset: '+6', name: 'Xinjiang Time (XJT)' },
{ offset: '+7', name: 'Indochina Time (ICT)' },
{ offset: '+8', name: 'Hong Kong Time (HKT)' },
{ offset: '+9', name: 'Japan Standard Time (JST)' },
{ offset: '+10', name: 'Australian Eastern Standard Time (AEST)' },
{ offset: '+11', name: 'Norfolk Time (NFT)' },
{ offset: '+12', name: 'New Zealand Standard Time (NZST)' },
{ offset: '+13', name: 'Tonga Time (TOT)' },
{ offset: '+14', name: 'Line Islands Time (LINT)' }
];

View file

@ -2,11 +2,11 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server'; import { ServerModule } from '@angular/platform-server';
import { ZONE_SERVICE } from './injection-tokens'; import { ZONE_SERVICE } from '@app/injection-tokens';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { AppComponent } from './components/app/app.component'; import { AppComponent } from '@components/app/app.component';
import { HttpCacheInterceptor } from './services/http-cache.interceptor'; import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
import { ZoneService } from './services/zone.service'; import { ZoneService } from '@app/services/zone.service';
@NgModule({ @NgModule({

View file

@ -2,33 +2,33 @@ import { BrowserModule } from '@angular/platform-browser';
import { ModuleWithProviders, NgModule } from '@angular/core'; import { ModuleWithProviders, NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ZONE_SERVICE } from './injection-tokens'; import { ZONE_SERVICE } from '@app/injection-tokens';
import { AppRoutingModule } from './app-routing.module'; 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 '@app/services/electrs-api.service';
import { OrdApiService } from './services/ord-api.service'; import { OrdApiService } from '@app/services/ord-api.service';
import { StateService } from './services/state.service'; import { StateService } from '@app/services/state.service';
import { CacheService } from './services/cache.service'; import { CacheService } from '@app/services/cache.service';
import { PriceService } from './services/price.service'; import { PriceService } from '@app/services/price.service';
import { EnterpriseService } from './services/enterprise.service'; import { EnterpriseService } from '@app/services/enterprise.service';
import { WebsocketService } from './services/websocket.service'; import { WebsocketService } from '@app/services/websocket.service';
import { AudioService } from './services/audio.service'; import { AudioService } from '@app/services/audio.service';
import { PreloadService } from './services/preload.service'; import { PreloadService } from '@app/services/preload.service';
import { SeoService } from './services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from './services/opengraph.service'; import { OpenGraphService } from '@app/services/opengraph.service';
import { ZoneService } from './services/zone-shim.service'; import { ZoneService } from '@app/services/zone-shim.service';
import { SharedModule } from './shared/shared.module'; import { SharedModule } from '@app/shared/shared.module';
import { StorageService } from './services/storage.service'; import { StorageService } from '@app/services/storage.service';
import { HttpCacheInterceptor } from './services/http-cache.interceptor'; import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
import { LanguageService } from './services/language.service'; import { LanguageService } from '@app/services/language.service';
import { ThemeService } from './services/theme.service'; import { ThemeService } from '@app/services/theme.service';
import { TimeService } from './services/time.service'; import { TimeService } from '@app/services/time.service';
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe'; import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe'; import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe'; import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe';
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe'; import { CapAddressPipe } from '@app/shared/pipes/cap-address-pipe/cap-address-pipe';
import { AppPreloadingStrategy } from './app.preloading-strategy'; import { AppPreloadingStrategy } from '@app/app.preloading-strategy';
import { ServicesApiServices } from './services/services-api.service'; import { ServicesApiServices } from '@app/services/services-api.service';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
const providers = [ const providers = [

View file

@ -1,13 +1,13 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { MasterPageComponent } from './components/master-page/master-page.component'; import { MasterPageComponent } from '@components/master-page/master-page.component';
const routes: Routes = [ const routes: Routes = [
{ {
path: '', path: '',
component: MasterPageComponent, component: MasterPageComponent,
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule), loadChildren: () => import('@app/graphs/graphs.module').then(m => m.GraphsModule),
data: { preload: true }, data: { preload: true },
} }
]; ];

View file

@ -1,5 +1,5 @@
import { Transaction, Vin } from './interfaces/electrs.interface'; import { Transaction, Vin } from '@interfaces/electrs.interface';
import { Hash } from './shared/sha256'; import { Hash } from '@app/shared/sha256';
const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH
const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH

View file

@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { EnterpriseService } from '../../services/enterprise.service'; import { EnterpriseService } from '@app/services/enterprise.service';
@Component({ @Component({
selector: 'app-about-sponsors', selector: 'app-about-sponsors',

View file

@ -201,12 +201,17 @@
<img class="image" src="/resources/profile/leather.svg" /> <img class="image" src="/resources/profile/leather.svg" />
<span>Leather</span> <span>Leather</span>
</a> </a>
<a href="https://taprootwizards.com/" target="_blank" title="Taproot Wizards">
<img class="image" src="/resources/profile/wizardhat.png" />
<span>Taproot Wizards</span>
</a>
</div> </div>
</div> </div>
<ng-container> <ng-container>
<div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor"> <div *ngIf="profiles$ | async as profiles" id="community-sponsors-anchor">
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0"> <div class="community-sponsor whale-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3> <h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
<div class="wrapper"> <div class="wrapper">
<ng-container> <ng-container>

View file

@ -92,6 +92,13 @@
} }
} }
.whale-sponsor {
img {
width: 70px;
height: 70px;
}
}
.alliances { .alliances {
margin-bottom: 100px; margin-bottom: 100px;
a { a {

View file

@ -1,16 +1,16 @@
import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core'; import { ChangeDetectionStrategy, Component, ElementRef, Inject, LOCALE_ID, OnInit, ViewChild } from '@angular/core';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '@app/services/websocket.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '../../services/opengraph.service'; import { OpenGraphService } from '@app/services/opengraph.service';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ApiService } from '../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { IBackendInfo } from '../../interfaces/websocket.interface'; import { IBackendInfo } from '@interfaces/websocket.interface';
import { Router, ActivatedRoute } from '@angular/router'; import { Router, ActivatedRoute } from '@angular/router';
import { map, share, tap } from 'rxjs/operators'; import { map, share, tap } from 'rxjs/operators';
import { ITranslators } from '../../interfaces/node-api.interface'; import { ITranslators } from '@interfaces/node-api.interface';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { EnterpriseService } from '../../services/enterprise.service'; import { EnterpriseService } from '@app/services/enterprise.service';
@Component({ @Component({
selector: 'app-about', selector: 'app-about',

View file

@ -1,9 +1,9 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { AboutComponent } from './about.component'; import { AboutComponent } from '@components/about/about.component';
import { AboutSponsorsComponent } from './about-sponsors.component'; import { AboutSponsorsComponent } from '@components/about/about-sponsors.component';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '@app/shared/shared.module';
const routes: Routes = [ const routes: Routes = [
{ {

View file

@ -172,10 +172,6 @@
background-color: var(--tertiary); background-color: var(--tertiary);
} }
.btn-small-height {
line-height: 1;
}
.summary-row { .summary-row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View file

@ -1,16 +1,16 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core'; import { Component, OnInit, OnDestroy, Output, EventEmitter, Input, ChangeDetectorRef, SimpleChanges, HostListener } from '@angular/core';
import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs'; import { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
import { ServicesApiServices } from '../../services/services-api.service'; import { ServicesApiServices } from '@app/services/services-api.service';
import { md5, insecureRandomUUID } from '../../shared/common.utils'; import { md5 } from '@app/shared/common.utils';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { AudioService } from '../../services/audio.service'; import { AudioService } from '@app/services/audio.service';
import { ETA, EtaService } from '../../services/eta.service'; import { ETA, EtaService } from '@app/services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface'; import { Transaction } from '@interfaces/electrs.interface';
import { MiningStats } from '../../services/mining.service'; import { MiningStats } from '@app/services/mining.service';
import { IAuth, AuthServiceMempool } from '../../services/auth.service'; import { IAuth, AuthServiceMempool } from '@app/services/auth.service';
import { EnterpriseService } from '../../services/enterprise.service'; import { EnterpriseService } from '@app/services/enterprise.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { isDevMode } from '@angular/core'; import { isDevMode } from '@angular/core';
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay'; export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
@ -84,13 +84,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
timePaid: number = 0; // time acceleration requested timePaid: number = 0; // time acceleration requested
math = Math; math = Math;
isMobile: boolean = window.innerWidth <= 767.98; isMobile: boolean = window.innerWidth <= 767.98;
isProdDomain = ['mempool.space', isProdDomain = false;
'mempool-staging.va1.mempool.space',
'mempool-staging.fmt.mempool.space',
'mempool-staging.fra.mempool.space',
'mempool-staging.tk7.mempool.space',
'mempool-staging.sg1.mempool.space'
].indexOf(document.location.hostname) > -1;
private _step: CheckoutStep = 'summary'; private _step: CheckoutStep = 'summary';
simpleMode: boolean = true; simpleMode: boolean = true;
@ -100,7 +94,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
auth: IAuth | null = null; auth: IAuth | null = null;
// accelerator stuff // accelerator stuff
accelerationUUID: string;
accelerationSubscription: Subscription; accelerationSubscription: Subscription;
difficultySubscription: Subscription; difficultySubscription: Subscription;
estimateSubscription: Subscription; estimateSubscription: Subscription;
@ -143,7 +136,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
private authService: AuthServiceMempool, private authService: AuthServiceMempool,
private enterpriseService: EnterpriseService, private enterpriseService: EnterpriseService,
) { ) {
this.accelerationUUID = insecureRandomUUID(); this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1;
// Check if Apple Pay available // Check if Apple Pay available
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview // https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
@ -207,6 +200,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
} }
moveToStep(step: CheckoutStep): void { moveToStep(step: CheckoutStep): void {
this.processing = false;
this._step = step; this._step = step;
if (this.timeoutTimer) { if (this.timeoutTimer) {
clearTimeout(this.timeoutTimer); clearTimeout(this.timeoutTimer);
@ -374,6 +368,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.selectFeeRateIndex = index; this.selectFeeRateIndex = index;
this.userBid = Math.max(0, fee); this.userBid = Math.max(0, fee);
this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee; this.cost = this.userBid + this.estimate.mempoolBaseFee + this.estimate.vsizeFee;
this.validateChoice();
} }
} }
@ -391,7 +386,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.accelerationSubscription = this.servicesApiService.accelerate$( this.accelerationSubscription = this.servicesApiService.accelerate$(
this.tx.txid, this.tx.txid,
this.userBid, this.userBid,
this.accelerationUUID
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false; this.processing = false;
@ -525,7 +519,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token, tokenResult.token,
cardTag, cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID costUSD
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false; this.processing = false;
@ -618,13 +612,21 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
this.processing = false; this.processing = false;
return; return;
} }
const verificationToken = await this.$verifyBuyer(this.payments, tokenResult.token, tokenResult.details, costUSD.toFixed(2));
if (!verificationToken) {
console.error(`SCA verification failed`);
this.accelerateError = 'SCA Verification Failed. Payment Declined.';
this.processing = false;
return;
}
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase()); const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
this.servicesApiService.accelerateWithGooglePay$( this.servicesApiService.accelerateWithGooglePay$(
this.tx.txid, this.tx.txid,
tokenResult.token, tokenResult.token,
verificationToken,
cardTag, cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID costUSD
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false; this.processing = false;
@ -714,7 +716,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token, tokenResult.token,
tokenResult.details.cashAppPay.cashtag, tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId, tokenResult.details.cashAppPay.referenceId,
this.accelerationUUID costUSD
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false; this.processing = false;
@ -749,6 +751,32 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
); );
} }
/**
* Required in SCA Mandated Regions: Learn more at https://developer.squareup.com/docs/sca-overview
*/
async $verifyBuyer(payments, token, details, amount) {
const verificationDetails = {
amount: amount,
currencyCode: 'USD',
intent: 'CHARGE',
billingContact: {
givenName: details.card?.billing?.givenName,
familyName: details.card?.billing?.familyName,
phone: details.card?.billing?.phone,
addressLines: details.card?.billing?.addressLines,
city: details.card?.billing?.city,
state: details.card?.billing?.state,
countryCode: details.card?.billing?.countryCode,
},
};
const verificationResults = await payments.verifyBuyer(
token,
verificationDetails,
);
return verificationResults.token;
}
/** /**
* BTCPay * BTCPay
*/ */

View file

@ -1,6 +1,6 @@
import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core'; import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { Transaction } from '../../interfaces/electrs.interface'; import { Transaction } from '@interfaces/electrs.interface';
import { AccelerationEstimate, RateOption } from './accelerate-checkout.component'; import { AccelerationEstimate, RateOption } from '@components/accelerate-checkout/accelerate-checkout.component';
interface GraphBar { interface GraphBar {
rate: number; rate: number;

View file

@ -1,6 +1,6 @@
<div class="acceleration-timeline box" [class.lower-padding]="!tx.status.confirmed"> <div class="acceleration-timeline box" [class.lower-padding]="!tx.status.confirmed">
<div class="timeline-wrapper"> <div class="timeline-wrapper">
@if (!tx.status.confirmed) { @if (!tx.status.confirmed || canceled) {
<div class="timeline"> <div class="timeline">
<div class="intervals"> <div class="intervals">
<div class="node-spacer"></div> <div class="node-spacer"></div>
@ -8,8 +8,8 @@
<div class="node-spacer"></div> <div class="node-spacer"></div>
<div class="interval"> <div class="interval">
<div class="interval-time"> <div class="interval-time">
@if (eta) { @if (eta && !canceled) {
~<app-time [time]="eta?.wait / 1000"></app-time> <!-- <span *ngIf="accelerateRatio > 1" class="compare"> ({{ accelerateRatio }}x faster)</span> --> ~<app-time [time]="eta?.wait / 1000"></app-time>
} }
</div> </div>
</div> </div>
@ -19,16 +19,20 @@
<div class="node-spacer"></div> <div class="node-spacer"></div>
<div class="interval-spacer"></div> <div class="interval-spacer"></div>
<div class="node"> <div class="node">
<div class="acc-to-confirmed right go-faster"></div> <div class="acc-to-confirmed right go-faster" [class.no-animation]="canceled"></div>
</div> </div>
<div class="interval-spacer"> <div class="interval-spacer">
</div> </div>
<div class="node" [id]="'confirmed'"> <div class="node" [id]="'confirmed'">
<div class="acc-to-confirmed left go-faster"></div> <div class="acc-to-confirmed left go-faster" [class.no-animation]="canceled"></div>
<div class="shape-border waiting"> <div class="shape-border waiting">
<div class="shape"></div> <div class="shape"></div>
</div> </div>
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div> @if (canceled) {
<div class="status"><span class="badge badge-danger" i18n="accelerator.canceled">Canceled</span></div>
} @else {
<div class="status"><span class="badge badge-waiting" i18n="transaction.rbf.mined">Mined</span></div>
}
</div> </div>
</div> </div>
</div> </div>
@ -45,11 +49,9 @@
<div class="interval"> <div class="interval">
<div class="interval-time"> <div class="interval-time">
@if (tx.status.confirmed) { @if (tx.status.confirmed) {
<div class="interval-time"> <app-time [time]="acceleratedToMined"></app-time>
<app-time [time]="acceleratedToMined"></app-time> } @else if (eta && canceled) {
</div> ~<app-time [time]="eta?.wait / 1000"></app-time>
} @else if (standardETA && !tx.status.confirmed) {
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
} }
</div> </div>
</div> </div>
@ -73,42 +75,42 @@
<div class="interval-spacer"> <div class="interval-spacer">
<div class="seen-to-acc"></div> <div class="seen-to-acc"></div>
</div> </div>
<div class="node" [class.accelerated]="!tx.status.confirmed" [id]="'accelerated'"> <div class="node" [class.accelerated]="!tx.status.confirmed && !canceled" [id]="'accelerated'">
<div class="seen-to-acc left"></div> <div class="seen-to-acc left"></div>
@if (tx.status.confirmed) { @if (tx.status.confirmed && !canceled) {
<div class="acc-to-confirmed right"></div> <div class="acc-to-confirmed right"></div>
} @else { } @else {
<div class="seen-to-acc right"></div> <div class="seen-to-acc right"></div>
} }
<div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);"> <div class="shape-border hovering" (pointerover)="onHover($event, 'accelerated');" (pointerout)="onBlur($event);">
<div class="shape"></div> <div class="shape"></div>
@if (!tx.status.confirmed) { @if (!tx.status.confirmed || canceled) {
<div class="connector down loading"></div> <div class="connector down" [class.loading]="!canceled"></div>
} }
</div> </div>
@if (tx.status.confirmed) { @if (tx.status.confirmed && !canceled) {
<div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div> <div class="status"><span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span></div>
} }
<div class="time" [class.no-margin]="!tx.status.confirmed" [class.offset-left]="!tx.status.confirmed"> <div class="time" [class.no-margin]="!tx.status.confirmed || canceled" [class.offset-left]="!tx.status.confirmed || canceled">
@if (!tx.status.confirmed) { @if (!tx.status.confirmed) {
<span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }} <span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }}
} }
@if (useAbsoluteTime) { @if (useAbsoluteTime) {
<span>{{ acceleratedAt * 1000 | date }}</span> <span>{{ acceleratedAt * 1000 | date }}</span>
} @else { } @else {
<app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="!tx.status.confirmed"></app-time> <app-time kind="since" [time]="acceleratedAt" [lowercaseStart]="!tx.status.confirmed || canceled"></app-time>
} }
</div> </div>
</div> </div>
<div class="interval-spacer"> <div class="interval-spacer">
@if (tx.status.confirmed) { @if (tx.status.confirmed && !canceled) {
<div class="acc-to-confirmed"></div> <div class="acc-to-confirmed"></div>
} @else { } @else {
<div class="seen-to-acc"></div> <div class="seen-to-acc"></div>
} }
</div> </div>
<div class="node" [class.selected]="tx.status.confirmed" [id]="'confirmed'"> <div class="node" [class.selected]="tx.status.confirmed" [id]="'confirmed'">
@if (tx.status.confirmed) { @if (tx.status.confirmed && !canceled) {
<div class="acc-to-confirmed left"></div> <div class="acc-to-confirmed left"></div>
} @else { } @else {
<div class="seen-to-acc left"></div> <div class="seen-to-acc left"></div>

View file

@ -129,6 +129,9 @@
margin-left: calc(-4em + 5px); margin-left: calc(-4em + 5px);
animation: goFasterLeft 0.8s infinite linear; animation: goFasterLeft 0.8s infinite linear;
} }
&.no-animation {
animation: none;
}
} }
&.left { &.left {

View file

@ -1,8 +1,8 @@
import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core'; import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core';
import { ETA } from '../../services/eta.service'; import { ETA } from '@app/services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface'; import { Transaction } from '@interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '../../interfaces/node-api.interface'; import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
import { MiningService } from '../../services/mining.service'; import { MiningService } from '@app/services/mining.service';
@Component({ @Component({
selector: 'app-acceleration-timeline', selector: 'app-acceleration-timeline',
@ -11,19 +11,15 @@ import { MiningService } from '../../services/mining.service';
}) })
export class AccelerationTimelineComponent implements OnInit, OnChanges { export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number; @Input() transactionTime: number;
@Input() acceleratedAt: number;
@Input() tx: Transaction; @Input() tx: Transaction;
@Input() accelerationInfo: Acceleration; @Input() accelerationInfo: Acceleration;
@Input() eta: ETA; @Input() eta: ETA;
// A mined transaction has standard ETA and accelerated ETA undefined @Input() canceled: boolean;
// A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet)
@Input() standardETA: number;
@Input() acceleratedETA: number;
acceleratedAt: number;
now: number; now: number;
accelerateRatio: number; accelerateRatio: number;
useAbsoluteTime: boolean = false; useAbsoluteTime: boolean = false;
interval: number;
firstSeenToAccelerated: number; firstSeenToAccelerated: number;
acceleratedToMined: number; acceleratedToMined: number;
@ -36,30 +32,17 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000; this.updateTimes();
this.miningService.getPools().subscribe(pools => { this.miningService.getPools().subscribe(pools => {
for (const pool of pools) { for (const pool of pools) {
this.poolsData[pool.unique_id] = pool; this.poolsData[pool.unique_id] = pool;
} }
}); });
this.updateTimes();
this.interval = window.setInterval(this.updateTimes.bind(this), 60000);
} }
ngOnChanges(changes): void { ngOnChanges(changes): void {
// Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65 this.updateTimes();
// if (changes?.eta?.currentValue || changes?.standardETA?.currentValue || changes?.acceleratedETA?.currentValue) {
// if (changes?.eta?.currentValue) {
// if (changes?.acceleratedETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.eta.currentValue.time / 1000) - this.now) / (Math.floor(changes.acceleratedETA.currentValue / 1000) - this.now));
// } else if (changes?.standardETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.standardETA.currentValue / 1000) - this.now) / (Math.floor(changes.eta.currentValue.time / 1000) - this.now));
// }
// }
// }
} }
updateTimes(): void { updateTimes(): void {
@ -69,10 +52,6 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
this.acceleratedToMined = Math.max(0, this.tx.status.block_time - this.acceleratedAt); this.acceleratedToMined = Math.max(0, this.tx.status.block_time - this.acceleratedAt);
} }
ngOnDestroy(): void {
clearInterval(this.interval);
}
onHover(event, status: string): void { onHover(event, status: string): void {
if (status === 'seen') { if (status === 'seen') {
this.hoverInfo = { this.hoverInfo = {

View file

@ -1,18 +1,18 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { EChartsOption } from '../../../graphs/echarts'; import { EChartsOption } from '@app/graphs/echarts';
import { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs'; import { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs';
import { startWith, switchMap, tap } from 'rxjs/operators'; import { startWith, switchMap, tap } from 'rxjs/operators';
import { SeoService } from '../../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { formatNumber } 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 '@app/shared/graphs.utils';
import { StorageService } from '../../../services/storage.service'; import { StorageService } from '@app/services/storage.service';
import { MiningService } from '../../../services/mining.service'; import { MiningService } from '@app/services/mining.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Acceleration } from '../../../interfaces/node-api.interface'; import { Acceleration } from '@interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service'; import { ServicesApiServices } from '@app/services/services-api.service';
import { StateService } from '../../../services/state.service'; import { StateService } from '@app/services/state.service';
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
@Component({ @Component({
selector: 'app-acceleration-fees-graph', selector: 'app-acceleration-fees-graph',

View file

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ServicesApiServices } from '../../../services/services-api.service'; import { ServicesApiServices } from '@app/services/services-api.service';
export type AccelerationStats = { export type AccelerationStats = {
totalRequested: number; totalRequested: number;

View file

@ -4,7 +4,7 @@
<div class="clearfix"></div> <div class="clearfix"></div>
<div class="acceleration-list"> <div class="acceleration-list" *ngIf="{ accelerations: accelerationList$ | async } as state">
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed"> <table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
<thead> <thead>
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th> <th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
@ -21,8 +21,8 @@
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th> <th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
</ng-container> </ng-container>
</thead> </thead>
<tbody *ngIf="accelerationList$ | async as accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> <tbody *ngIf="state.accelerations && nonEmptyAccelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
<tr *ngFor="let acceleration of accelerations; let i= index;"> <tr *ngFor="let acceleration of state.accelerations; let i= index;">
<td class="txid text-left"> <td class="txid text-left">
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]"> <a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
<app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate> <app-truncate [text]="acceleration.txid" [lastChars]="5"></app-truncate>

View file

@ -1,12 +1,12 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core'; import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy, Inject, LOCALE_ID } from '@angular/core';
import { BehaviorSubject, Observable, Subscription, catchError, combineLatest, filter, of, switchMap, tap, throttleTime, timer } from 'rxjs'; import { BehaviorSubject, Observable, Subscription, catchError, combineLatest, filter, of, switchMap, tap, throttleTime, timer } from 'rxjs';
import { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface'; import { Acceleration, BlockExtended, SinglePoolStats } from '@interfaces/node-api.interface';
import { StateService } from '../../../services/state.service'; import { StateService } from '@app/services/state.service';
import { WebsocketService } from '../../../services/websocket.service'; import { WebsocketService } from '@app/services/websocket.service';
import { ServicesApiServices } from '../../../services/services-api.service'; import { ServicesApiServices } from '@app/services/services-api.service';
import { SeoService } from '../../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { MiningService } from '../../../services/mining.service'; import { MiningService } from '@app/services/mining.service';
@Component({ @Component({
selector: 'app-accelerations-list', selector: 'app-accelerations-list',

View file

@ -1,18 +1,18 @@
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { SeoService } from '../../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '../../../services/opengraph.service'; import { OpenGraphService } from '@app/services/opengraph.service';
import { WebsocketService } from '../../../services/websocket.service'; import { WebsocketService } from '@app/services/websocket.service';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; import { Acceleration, BlockExtended } from '@interfaces/node-api.interface';
import { StateService } from '../../../services/state.service'; import { StateService } from '@app/services/state.service';
import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs'; import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs';
import { Color } from '../../block-overview-graph/sprite-types'; import { Color } from '@components/block-overview-graph/sprite-types';
import { hexToColor } from '../../block-overview-graph/utils'; import { hexToColor } from '@components/block-overview-graph/utils';
import TxView from '../../block-overview-graph/tx-view'; import TxView from '@components/block-overview-graph/tx-view';
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../../app.constants'; import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '@app/app.constants';
import { ServicesApiServices } from '../../../services/services-api.service'; import { ServicesApiServices } from '@app/services/services-api.service';
import { detectWebGL } from '../../../shared/graphs.utils'; import { detectWebGL } from '@app/shared/graphs.utils';
import { AudioService } from '../../../services/audio.service'; import { AudioService } from '@app/services/audio.service';
import { ThemeService } from '../../../services/theme.service'; import { ThemeService } from '@app/services/theme.service';
const acceleratedColor: Color = hexToColor('8F5FF6'); const acceleratedColor: Color = hexToColor('8F5FF6');
const normalColors = defaultMempoolFeeColors.map(hex => hexToColor(hex + '5F')); const normalColors = defaultMempoolFeeColors.map(hex => hexToColor(hex + '5F'));

View file

@ -20,7 +20,7 @@
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft"> <td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
@if (hasCpfp) { @if (hasCpfp) {
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button> <button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
} }
<ng-container *ngTemplateOutlet="pieChart"></ng-container> <ng-container *ngTemplateOutlet="pieChart"></ng-container>
</div> </div>
@ -36,7 +36,7 @@
<tr> <tr>
<td colspan="3" class="pt-0"> <td colspan="3" class="pt-0">
<div class="d-flex justify-content-end align-items-start"> <div class="d-flex justify-content-end align-items-start">
<button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP <fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></button> <button type="button" class="btn btn-outline-info btn-sm btn-small-height float-right mt-0" (click)="onToggleCpfp()">CPFP</button>
</div> </div>
</td> </td>
</tr> </tr>

View file

@ -1,8 +1,8 @@
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter, ChangeDetectorRef } from '@angular/core'; import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter, ChangeDetectorRef } from '@angular/core';
import { Transaction } from '../../../interfaces/electrs.interface'; import { Transaction } from '@interfaces/electrs.interface';
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface'; import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts'; import { EChartsOption, PieSeriesOption } from '@app/graphs/echarts';
import { MiningStats } from '../../../services/mining.service'; import { MiningStats } from '@app/services/mining.service';
function lighten(color, p): { r, g, b } { function lighten(color, p): { r, g, b } {
return { return {
@ -76,15 +76,21 @@ export class ActiveAccelerationBox implements OnChanges {
acceleratingPools.forEach((poolId, index) => { acceleratingPools.forEach((poolId, index) => {
const pool = pools[poolId]; const pool = pools[poolId];
const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1); const poolShare = ((pool.lastEstimatedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1);
let color = 'white';
if (index >= firstSignificantPool) {
if (numSignificantPools > 1) {
color = toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / Math.max((numSignificantPools - 1), 1)));
} else {
color = toRGB({ r: 147, g: 57, b: 244 });
}
}
data.push(getDataItem( data.push(getDataItem(
pool.lastEstimatedHashrate, pool.lastEstimatedHashrate,
index >= firstSignificantPool color,
? toRGB(lighten({ r: 147, g: 57, b: 244 }, 1 - (index - firstSignificantPool) / (numSignificantPools - 1)))
: 'white',
`<b style="color: white">${pool.name} (${poolShare}%)</b>`, `<b style="color: white">${pool.name} (${poolShare}%)</b>`,
true, true,
) as PieSeriesOption); ) as PieSeriesOption);
}) });
this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%'; this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%';
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%'; const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
data.push(getDataItem( data.push(getDataItem(

View file

@ -1,9 +1,9 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { Acceleration } from '../../../interfaces/node-api.interface'; import { Acceleration } from '@interfaces/node-api.interface';
import { StateService } from '../../../services/state.service'; import { StateService } from '@app/services/state.service';
import { WebsocketService } from '../../../services/websocket.service'; import { WebsocketService } from '@app/services/websocket.service';
@Component({ @Component({
selector: 'app-pending-stats', selector: 'app-pending-stats',

View file

@ -1,16 +1,15 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts'; import { echarts, EChartsOption } from '@app/graphs/echarts';
import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs'; import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface'; import { AddressTxSummary, ChainStats } from '@interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { PriceService } from '../../services/price.service'; import { PriceService } from '@app/services/price.service';
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe'; import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
const periodSeconds = { const periodSeconds = {
'1d': (60 * 60 * 24), '1d': (60 * 60 * 24),
@ -45,14 +44,18 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
@Input() right: number | string = 10; @Input() right: number | string = 10;
@Input() left: number | string = 70; @Input() left: number | string = 70;
@Input() widget: boolean = false; @Input() widget: boolean = false;
@Input() defaultFiat: boolean = false;
@Input() showLegend: boolean = true;
@Input() showYAxis: boolean = true;
adjustedLeft: number;
adjustedRight: number;
data: any[] = []; data: any[] = [];
fiatData: any[] = []; fiatData: any[] = [];
hoverData: any[] = []; hoverData: any[] = [];
conversions: any; conversions: any;
allowZoom: boolean = false; allowZoom: boolean = false;
initialRight = this.right;
initialLeft = this.left;
selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false }; selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false };
subscription: Subscription; subscription: Subscription;
@ -77,15 +80,17 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
private relativeUrlPipe: RelativeUrlPipe, private relativeUrlPipe: RelativeUrlPipe,
private priceService: PriceService, private priceService: PriceService,
private fiatCurrencyPipe: FiatCurrencyPipe, private fiatCurrencyPipe: FiatCurrencyPipe,
private fiatShortenerPipe: FiatShortenerPipe,
private zone: NgZone, private zone: NgZone,
) {} ) {}
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true; this.isLoading = true;
if (!this.address || !this.stats) { if (!this.addressSummary$ && (!this.address || !this.stats)) {
return; return;
} }
if (changes.defaultFiat) {
this.selected['Fiat'] = !!this.defaultFiat;
}
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) { if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
if (this.subscription) { if (this.subscription) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
@ -118,7 +123,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
} else if (this.conversions && this.conversions['USD']) { } else if (this.conversions && this.conversions['USD']) {
price = this.conversions['USD']; price = this.conversions['USD'];
} }
return { ...item, price: price } return { ...item, price: price };
}); });
} }
}), }),
@ -144,15 +149,16 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
} }
prepareChartOptions(summary: AddressTxSummary[]) { prepareChartOptions(summary: AddressTxSummary[]) {
if (!summary || !this.stats) { if (!summary) {
return; return;
} }
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum); const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0);
let runningTotal = total;
const processData = summary.map(d => { const processData = summary.map(d => {
const balance = total; const balance = runningTotal;
const fiatBalance = total * d.price / 100_000_000; const fiatBalance = runningTotal * d.price / 100_000_000;
total -= d.value; runningTotal -= d.value;
return { return {
time: d.time * 1000, time: d.time * 1000,
balance, balance,
@ -172,12 +178,15 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
this.fiatData = this.fiatData.filter(d => d[0] >= startFiat); this.fiatData = this.fiatData.filter(d => d[0] >= startFiat);
} }
this.data.push( this.data.push(
{value: [now, this.stats.funded_txo_sum - this.stats.spent_txo_sum], symbol: 'none', tooltip: { show: false }} {value: [now, total], symbol: 'none', tooltip: { show: false }}
); );
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0); const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);
const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue); const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue);
this.adjustedRight = this.selected['Fiat'] ? +this.right + 40 : +this.right;
this.adjustedLeft = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? +this.left : +this.left - 40;
this.chartOptions = { this.chartOptions = {
color: [ color: [
new echarts.graphic.LinearGradient(0, 0, 0, 1, [ new echarts.graphic.LinearGradient(0, 0, 0, 1, [
@ -193,10 +202,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
grid: { grid: {
top: 20, top: 20,
bottom: this.allowZoom ? 65 : 20, bottom: this.allowZoom ? 65 : 20,
right: this.right, right: this.adjustedRight,
left: this.left, left: this.adjustedLeft,
}, },
legend: !this.stateService.isAnyTestnet() ? { legend: (this.showLegend && !this.stateService.isAnyTestnet()) ? {
data: [ data: [
{ {
name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`, name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`,
@ -244,18 +253,19 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
let tooltip = '<div>'; let tooltip = '<div>';
const hasTx = data[0].data[2].txid; const hasTx = data[0].data[2].txid;
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
tooltip += `<div>
<div style="text-align: right;">
<div><b>${date}</b></div>`;
if (hasTx) { if (hasTx) {
const header = data.length === 1 const header = data.length === 1
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}` ? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
: `${data.length} transactions`; : `${data.length} transactions`;
tooltip += `<span><b>${header}</b></span>`; tooltip += `<div><b>${header}</b></div>`;
} }
const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
tooltip += `<div>
<div style="text-align: right;">`;
const formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal); const formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal);
const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD'); const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD');
@ -290,7 +300,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
} }
} }
tooltip += `</div><span>${date}</span></div>`; tooltip += `</div></div>`;
return tooltip; return tooltip;
}.bind(this) }.bind(this)
}, },
@ -306,22 +316,26 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
type: 'value', type: 'value',
position: 'left', position: 'left',
axisLabel: { axisLabel: {
show: this.showYAxis,
color: 'rgb(110, 112, 121)', color: 'rgb(110, 112, 121)',
formatter: (val): string => { formatter: (val): string => {
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue); let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
if (valSpan > 100_000_000_000) { if (valSpan > 100_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0)} BTC`; return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 0, undefined, true)} BTC`;
} }
else if (valSpan > 1_000_000_000) { else if (valSpan > 1_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2)} BTC`; return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 2, undefined, true)} BTC`;
} else if (valSpan > 100_000_000) { } else if (valSpan > 100_000_000) {
return `${(val / 100_000_000).toFixed(1)} BTC`; return `${(val / 100_000_000).toFixed(1)} BTC`;
} else if (valSpan > 10_000_000) { } else if (valSpan > 10_000_000) {
return `${(val / 100_000_000).toFixed(2)} BTC`; return `${(val / 100_000_000).toFixed(2)} BTC`;
} else if (valSpan > 1_000_000) { } else if (valSpan > 1_000_000) {
if (maxValue > 100_000_000_000) {
return `${this.amountShortenerPipe.transform(Math.round(val / 100_000_000), 3, undefined, true)} BTC`;
}
return `${(val / 100_000_000).toFixed(3)} BTC`; return `${(val / 100_000_000).toFixed(3)} BTC`;
} else { } else {
return `${this.amountShortenerPipe.transform(val, 0)} sats`; return `${this.amountShortenerPipe.transform(val, 0, undefined, true)} sats`;
} }
} }
}, },
@ -333,9 +347,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
{ {
type: 'value', type: 'value',
axisLabel: { axisLabel: {
show: this.showYAxis,
color: 'rgb(110, 112, 121)', color: 'rgb(110, 112, 121)',
formatter: function(val) { formatter: function(val) {
return this.fiatShortenerPipe.transform(val, null, 'USD'); return `$${this.amountShortenerPipe.transform(val, 3, undefined, true, true)}`;
}.bind(this) }.bind(this)
}, },
splitLine: { splitLine: {
@ -389,8 +404,8 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
type: 'slider', type: 'slider',
brushSelect: false, brushSelect: false,
realtime: true, realtime: true,
left: this.left, left: this.adjustedLeft,
right: this.right, right: this.adjustedRight,
selectedDataBackground: { selectedDataBackground: {
lineStyle: { lineStyle: {
color: '#fff', color: '#fff',
@ -420,23 +435,23 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
onLegendSelectChanged(e) { onLegendSelectChanged(e) {
this.selected = e.selected; this.selected = e.selected;
this.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight; this.adjustedRight = this.selected['Fiat'] ? +this.right + 40 : +this.right;
this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40; this.adjustedLeft = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? +this.left : +this.left - 40;
this.chartOptions = { this.chartOptions = {
grid: { grid: {
right: this.right, right: this.adjustedRight,
left: this.left, left: this.adjustedLeft,
}, },
legend: { legend: {
selected: this.selected, selected: this.selected,
}, },
dataZoom: this.allowZoom ? [{ dataZoom: this.allowZoom ? [{
left: this.left, left: this.adjustedLeft,
right: this.right, right: this.adjustedRight,
}, { }, {
left: this.left, left: this.adjustedLeft,
right: this.right, right: this.adjustedRight,
}] : undefined }] : undefined
}; };

View file

@ -1,15 +1,15 @@
import { Component, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core'; import { Component, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, catchError } from 'rxjs/operators'; import { switchMap, catchError } from 'rxjs/operators';
import { Address, Transaction } from '../../interfaces/electrs.interface'; import { Address, Transaction } from '@interfaces/electrs.interface';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '@app/services/websocket.service';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { AudioService } from '../../services/audio.service'; import { AudioService } from '@app/services/audio.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { of, Subscription, forkJoin } from 'rxjs'; import { of, Subscription, forkJoin } from 'rxjs';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { AddressInformation } from '../../interfaces/node-api.interface'; import { AddressInformation } from '@interfaces/node-api.interface';
@Component({ @Component({
selector: 'app-address-group', selector: 'app-address-group',

View file

@ -1,7 +1,7 @@
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core'; import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
import { Vin, Vout } from '../../interfaces/electrs.interface'; import { Vin, Vout } from '@interfaces/electrs.interface';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { AddressType, AddressTypeInfo } from '../../shared/address-utils'; import { AddressType, AddressTypeInfo } from '@app/shared/address-utils';
@Component({ @Component({
selector: 'app-address-labels', selector: 'app-address-labels',

View file

@ -12,7 +12,7 @@
<app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate> <app-truncate [text]="transaction.txid" [lastChars]="5"></app-truncate>
</a> </a>
</td> </td>
<td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" digitsInfo="1.2-4" [noFiat]="true"></app-amount></td> <td class="table-cell-satoshis"><app-amount [satoshis]="transaction.value" [digitsInfo]="getAmountDigits(transaction.value)" [noFiat]="true"></app-amount></td>
<td class="table-cell-fiat" ><app-fiat [value]="transaction.value" [blockConversion]="transaction.price" digitsInfo="1.0-0"></app-fiat></td> <td class="table-cell-fiat" ><app-fiat [value]="transaction.value" [blockConversion]="transaction.price" digitsInfo="1.0-0"></app-fiat></td>
<td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true" [showTooltip]="true"></app-time></td> <td class="table-cell-date"><app-time kind="since" [time]="transaction.time" [fastRender]="true" [showTooltip]="true"></app-time></td>
</tr> </tr>

View file

@ -1,9 +1,9 @@
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface'; import { Address, AddressTxSummary } from '@interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { Observable, Subscription, catchError, map, of, switchMap, zip } from 'rxjs'; import { Observable, Subscription, catchError, map, of, switchMap, zip } from 'rxjs';
import { PriceService } from '../../services/price.service'; import { PriceService } from '@app/services/price.service';
@Component({ @Component({
selector: 'app-address-transactions-widget', selector: 'app-address-transactions-widget',
@ -43,7 +43,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
startAddressSubscription(): void { startAddressSubscription(): void {
this.isLoading = true; this.isLoading = true;
if (!this.address || !this.addressInfo) { if (!this.addressSummary$ && (!this.address || !this.addressInfo)) {
return; return;
} }
this.transactions$ = (this.addressSummary$ || (this.isPubkey this.transactions$ = (this.addressSummary$ || (this.isPubkey
@ -55,7 +55,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
}) })
)).pipe( )).pipe(
map(summary => { map(summary => {
return summary?.slice(0, 6); return summary?.filter(tx => Math.abs(tx.value) >= 1000000)?.slice(0, 6);
}), }),
switchMap(txs => { switchMap(txs => {
return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe( return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe(
@ -68,6 +68,12 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On
)))); ))));
}) })
); );
}
getAmountDigits(value: number): string {
const decimals = Math.max(0, 4 - Math.ceil(Math.log10(Math.abs(value / 100_000_000))));
return `1.${decimals}-${decimals}`;
} }
ngOnDestroy(): void { ngOnDestroy(): void {

View file

@ -1,16 +1,16 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators'; import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
import { Address, Transaction } from '../../interfaces/electrs.interface'; import { Address, Transaction } from '@interfaces/electrs.interface';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { OpenGraphService } from '../../services/opengraph.service'; import { OpenGraphService } from '@app/services/opengraph.service';
import { AudioService } from '../../services/audio.service'; import { AudioService } from '@app/services/audio.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { of, merge, Subscription, Observable } from 'rxjs'; import { of, merge, Subscription, Observable } from 'rxjs';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils'; import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface'; import { AddressInformation } from '@interfaces/node-api.interface';
@Component({ @Component({
selector: 'app-address-preview', selector: 'app-address-preview',

View file

@ -117,7 +117,7 @@
</h2> </h2>
</div> </div>
<app-transactions-list [transactions]="transactions" [showConfirmations]="true" [address]="address.address" (loadMore)="loadMore()"></app-transactions-list> <app-transactions-list [transactions]="transactions" [showConfirmations]="true" [addresses]="[address.address]" (loadMore)="loadMore()"></app-transactions-list>
<div class="text-center"> <div class="text-center">
<ng-template [ngIf]="isLoadingTransactions"> <ng-template [ngIf]="isLoadingTransactions">

View file

@ -1,17 +1,17 @@
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators'; import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
import { Address, ChainStats, Transaction, Utxo, Vin } from '../../interfaces/electrs.interface'; import { Address, ChainStats, Transaction, Utxo, Vin } from '@interfaces/electrs.interface';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '@app/services/websocket.service';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { AudioService } from '../../services/audio.service'; import { AudioService } from '@app/services/audio.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { of, merge, Subscription, Observable, forkJoin } from 'rxjs'; import { of, merge, Subscription, Observable, forkJoin } from 'rxjs';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { seoDescriptionNetwork } from '../../shared/common.utils'; import { seoDescriptionNetwork } from '@app/shared/common.utils';
import { AddressInformation } from '../../interfaces/node-api.interface'; import { AddressInformation } from '@interfaces/node-api.interface';
import { AddressTypeInfo } from '../../shared/address-utils'; import { AddressTypeInfo } from '@app/shared/address-utils';
class AddressStats implements ChainStats { class AddressStats implements ChainStats {
address: string; address: string;

View file

@ -0,0 +1,10 @@
<div class="addresses-treemap-container">
<div *ngIf="addresses" style="height: 300px">
<div *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
</div>
</div>
<div *ngIf="!stateService.isBrowser || isLoading" class="text-center loading-spinner">
<div class="spinner-border text-light"></div>
</div>
</div>

View file

@ -0,0 +1,17 @@
.node-channels-container {
position: relative;
}
.loading-spinner {
position: absolute;
top: 0;
left: 0;
right: 0;
width: 100%;
z-index: 100;
}
.spinner-border {
position: relative;
top: 225px;
}

View file

@ -0,0 +1,150 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges } from '@angular/core';
import { Router } from '@angular/router';
import { EChartsOption, TreemapSeriesOption } from '@app/graphs/echarts';
import { lerpColor } from '@app/shared/graphs.utils';
import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
import { LightningApiService } from '@app/lightning/lightning-api.service';
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '@app/services/state.service';
import { Address } from '@interfaces/electrs.interface';
import { formatNumber } from '@angular/common';
@Component({
selector: 'app-addresses-treemap',
templateUrl: './addresses-treemap.component.html',
styleUrls: ['./addresses-treemap.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddressesTreemap implements OnChanges {
@Input() addresses: Address[];
@Input() isLoading: boolean = false;
chartInstance: any;
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
};
constructor(
@Inject(LOCALE_ID) public locale: string,
private lightningApiService: LightningApiService,
private amountShortenerPipe: AmountShortenerPipe,
private zone: NgZone,
private router: Router,
public stateService: StateService,
) {}
ngOnChanges(): void {
this.prepareChartOptions();
}
prepareChartOptions(): void {
const data = this.addresses.map(address => ({
address: address.address,
value: address.chain_stats.funded_txo_sum - address.chain_stats.spent_txo_sum,
stats: address.chain_stats,
}));
// only consider visible items for the color gradient
const totalValue = data.reduce((acc, address) => acc + address.value, 0);
const maxTxs = data.filter(address => address.value > (totalValue / 2000)).reduce((max, address) => Math.max(max, address.stats.tx_count), 0);
const dataItems = data.map(address => ({
...address,
itemStyle: {
color: lerpColor('#1E88E5', '#D81B60', address.stats.tx_count / maxTxs),
}
}));
this.chartOptions = {
tooltip: {
trigger: 'item',
textStyle: {
align: 'left',
}
},
series: <TreemapSeriesOption[]>[
{
height: 300,
left: 0,
right: 0,
bottom: 0,
top: 0,
roam: false,
type: 'treemap',
data: dataItems,
nodeClick: 'link',
progressive: 100,
tooltip: {
show: true,
backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 4,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: {
color: '#b1b1b1',
},
borderColor: '#000',
formatter: (value): string => {
if (!value.data.address) {
return '';
}
return `
<table style="table-layout: fixed;">
<tbody>
<tr>
<td colspan="2"><b style="color: white; margin-left: 2px">${value.data.address}</b></td>
</tr>
<tr>
<td>Received</td>
<td style="text-align: right">${this.formatValue(value.data.stats.funded_txo_sum)}</td>
</tr>
<tr>
<td>Sent</td>
<td style="text-align: right">${this.formatValue(value.data.stats.spent_txo_sum)}</td>
</tr>
<tr>
<td>Balance</td>
<td style="text-align: right">${this.formatValue(value.data.stats.funded_txo_sum - value.data.stats.spent_txo_sum)}</td>
</tr>
<tr>
<td>Transaction count</td>
<td style="text-align: right">${value.data.stats.tx_count}</td>
</tr>
</tbody>
</table>
`;
}
},
itemStyle: {
borderColor: 'black',
borderWidth: 1,
},
breadcrumb: {
show: false,
}
}
]
};
}
formatValue(sats: number): string {
if (sats > 100000000) {
return formatNumber(sats / 100000000, this.locale, '1.2-2') + ' BTC';
} else {
return this.amountShortenerPipe.transform(sats, 2) + ' sats';
}
}
onChartInit(ec: any): void {
this.chartInstance = ec;
this.chartInstance.on('click', (e) => {
//@ts-ignore
if (!e.data.address) {
return;
}
this.zone.run(() => {
//@ts-ignore
const url = new RelativeUrlPipe(this.stateService).transform(`/address/${e.data.address}`);
this.router.navigate([url]);
});
});
}
}

View file

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '@app/services/storage.service';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
@Component({ @Component({
selector: 'app-amount-selector', selector: 'app-amount-selector',

View file

@ -1,7 +1,7 @@
import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { Price } from '../../services/price.service'; import { Price } from '@app/services/price.service';
@Component({ @Component({
selector: 'app-amount', selector: 'app-amount',

View file

@ -1,11 +1,11 @@
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { Component, HostListener, OnInit, Inject, LOCALE_ID, HostBinding } from '@angular/core'; import { Component, HostListener, OnInit, Inject, LOCALE_ID, HostBinding } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router'; import { Router, NavigationEnd } from '@angular/router';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { OpenGraphService } from '../../services/opengraph.service'; import { OpenGraphService } from '@app/services/opengraph.service';
import { NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap'; import { NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap';
import { ThemeService } from '../../services/theme.service'; import { ThemeService } from '@app/services/theme.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',

View file

@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { moveDec } from '../../bitcoin.utils'; import { moveDec } from '@app/bitcoin.utils';
import { AssetsService } from '../../services/assets.service'; import { AssetsService } from '@app/services/assets.service';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { environment } from '../../../environments/environment'; import { environment } from '@environments/environment';
@Component({ @Component({
selector: 'app-asset-circulation', selector: 'app-asset-circulation',

View file

@ -1,17 +1,17 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { switchMap, filter, catchError, take } from 'rxjs/operators'; import { switchMap, filter, catchError, take } from 'rxjs/operators';
import { Asset, Transaction } from '../../interfaces/electrs.interface'; import { Asset, Transaction } from '@interfaces/electrs.interface';
import { WebsocketService } from '../../services/websocket.service'; import { WebsocketService } from '@app/services/websocket.service';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { AudioService } from '../../services/audio.service'; import { AudioService } from '@app/services/audio.service';
import { ApiService } from '../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { of, merge, Subscription, combineLatest } from 'rxjs'; import { of, merge, Subscription, combineLatest } from 'rxjs';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { environment } from '../../../environments/environment'; import { environment } from '@environments/environment';
import { AssetsService } from '../../services/assets.service'; import { AssetsService } from '@app/services/assets.service';
import { moveDec } from '../../bitcoin.utils'; import { moveDec } from '@app/bitcoin.utils';
@Component({ @Component({
selector: 'app-asset', selector: 'app-asset',

View file

@ -2,8 +2,8 @@ import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { ApiService } from '../../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { AssetsService } from '../../../services/assets.service'; import { AssetsService } from '@app/services/assets.service';
@Component({ @Component({
selector: 'app-asset-group', selector: 'app-asset-group',

View file

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ApiService } from '../../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { StateService } from '../../../services/state.service'; import { StateService } from '@app/services/state.service';
@Component({ @Component({
selector: 'app-assets-featured', selector: 'app-assets-featured',

View file

@ -4,12 +4,12 @@ import { Router } from '@angular/router';
import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; import { NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
import { merge, Observable, of, Subject } from 'rxjs'; import { merge, Observable, of, Subject } from 'rxjs';
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
import { AssetExtended } from '../../../interfaces/electrs.interface'; import { AssetExtended } from '@interfaces/electrs.interface';
import { AssetsService } from '../../../services/assets.service'; import { AssetsService } from '@app/services/assets.service';
import { SeoService } from '../../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { StateService } from '../../../services/state.service'; import { StateService } from '@app/services/state.service';
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { environment } from '../../../../environments/environment'; import { environment } from '@environments/environment';
@Component({ @Component({
selector: 'app-assets-nav', selector: 'app-assets-nav',

View file

@ -1,13 +1,13 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core'; import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { AssetsService } from '../../services/assets.service'; import { AssetsService } from '@app/services/assets.service';
import { environment } from '../../../environments/environment'; import { environment } from '@environments/environment';
import { UntypedFormGroup } from '@angular/forms'; import { UntypedFormGroup } from '@angular/forms';
import { filter, map, switchMap, take } from 'rxjs/operators'; import { filter, map, switchMap, take } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { AssetExtended } from '../../interfaces/electrs.interface'; import { AssetExtended } from '@interfaces/electrs.interface';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
@Component({ @Component({
selector: 'app-assets', selector: 'app-assets',

View file

@ -4,10 +4,10 @@
<div class="item"> <div class="item">
<h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5> <h5 class="card-title" i18n="dashboard.btc-holdings">BTC Holdings</h5>
<div class="card-text"> <div class="card-text">
{{ ((addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span> {{ ((total) / 100_000_000) | number: '1.2-2' }} <span class="symbol" i18n="shared.btc|BTC">BTC</span>
</div> </div>
<div class="symbol"> <div class="symbol">
<app-fiat [value]="(addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum)"></app-fiat> <app-fiat [value]="(total)"></app-fiat>
</div> </div>
</div> </div>
<div class="item"> <div class="item">

View file

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface'; import { Address, AddressTxSummary } from '@interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '@app/services/electrs-api.service';
import { Observable, catchError, of } from 'rxjs'; import { Observable, catchError, of } from 'rxjs';
@Component({ @Component({
@ -19,6 +19,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
isLoading: boolean = true; isLoading: boolean = true;
error: any; error: any;
total: number = 0;
delta7d: number = 0; delta7d: number = 0;
delta30d: number = 0; delta30d: number = 0;
@ -34,7 +35,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true; this.isLoading = true;
if (!this.address || !this.addressInfo) { if (!this.addressSummary$ && (!this.address || !this.addressInfo)) {
return; return;
} }
(this.addressSummary$ || (this.isPubkey (this.addressSummary$ || (this.isPubkey
@ -57,6 +58,7 @@ export class BalanceWidgetComponent implements OnInit, OnChanges {
calculateStats(summary: AddressTxSummary[]): void { calculateStats(summary: AddressTxSummary[]): void {
let weekTotal = 0; let weekTotal = 0;
let monthTotal = 0; let monthTotal = 0;
this.total = this.addressInfo ? this.addressInfo.chain_stats.funded_txo_sum - this.addressInfo.chain_stats.spent_txo_sum : summary.reduce((acc, tx) => acc + tx.value, 0);
const weekAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (7 * 24 * 60 * 60 * 1000)).getTime()) / 1000; const weekAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (7 * 24 * 60 * 60 * 1000)).getTime()) / 1000;
const monthAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (30 * 24 * 60 * 60 * 1000)).getTime()) / 1000; const monthAgo = (new Date(new Date().setHours(0, 0, 0, 0) - (30 * 24 * 60 * 60 * 1000)).getTime()) / 1000;

View file

@ -4,7 +4,7 @@ import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Subscription, of, timer } from 'rxjs'; import { Subscription, of, timer } from 'rxjs';
import { filter, repeat, retry, switchMap, take, tap } from 'rxjs/operators'; import { filter, repeat, retry, switchMap, take, tap } from 'rxjs/operators';
import { ServicesApiServices } from '../../services/services-api.service'; import { ServicesApiServices } from '@app/services/services-api.service';
@Component({ @Component({
selector: 'app-bitcoin-invoice', selector: 'app-bitcoin-invoice',

View file

@ -1,17 +1,17 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts'; import { echarts, EChartsOption } from '@app/graphs/echarts';
import { Observable, combineLatest, of } from 'rxjs'; import { Observable, combineLatest, of } 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 '@app/services/api.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { formatNumber } 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 '@app/shared/graphs.utils';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '@app/services/storage.service';
import { MiningService } from '../../services/mining.service'; import { MiningService } from '@app/services/mining.service';
import { selectPowerOfTen } from '../../bitcoin.utils'; import { selectPowerOfTen } from '@app/bitcoin.utils';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@Component({ @Component({

View file

@ -1,18 +1,18 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts'; import { echarts, EChartsOption } from '@app/graphs/echarts';
import { Observable } from 'rxjs'; import { Observable } 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 '@app/services/api.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { formatNumber } from '@angular/common'; import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis } from '../../shared/graphs.utils'; import { download, formatterXAxis } from '@app/shared/graphs.utils';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '@app/services/storage.service';
import { MiningService } from '../../services/mining.service'; import { MiningService } from '@app/services/mining.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe'; import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe'; import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
@Component({ @Component({
selector: 'app-block-fees-graph', selector: 'app-block-fees-graph',

View file

@ -1,19 +1,19 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
import { EChartsOption } from '../../graphs/echarts'; import { EChartsOption } from '@app/graphs/echarts';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { catchError, map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { catchError, map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service'; import { ApiService } from '@app/services/api.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { formatNumber } from '@angular/common'; import { formatNumber } from '@angular/common';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { download, formatterXAxis } from '../../shared/graphs.utils'; import { download, formatterXAxis } from '@app/shared/graphs.utils';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe'; import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe'; import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { MiningService } from '../../services/mining.service'; import { MiningService } from '@app/services/mining.service';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '@app/services/storage.service';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
@Component({ @Component({
selector: 'app-block-fees-subsidy-graph', selector: 'app-block-fees-subsidy-graph',

View file

@ -1,6 +1,6 @@
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core'; import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
import { ActiveFilter, FilterGroups, FilterMode, GradientMode, TransactionFilters } from '../../shared/filters.utils'; import { ActiveFilter, FilterGroups, FilterMode, GradientMode, TransactionFilters } from '@app/shared/filters.utils';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';

View file

@ -1,16 +1,16 @@
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
import { EChartsOption } from '../../graphs/echarts'; import { EChartsOption } from '@app/graphs/echarts';
import { Observable } from 'rxjs'; import { Observable } 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 '@app/services/api.service';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '@app/services/seo.service';
import { formatNumber } 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 '@app/shared/graphs.utils';
import { StorageService } from '../../services/storage.service'; import { StorageService } from '@app/services/storage.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
@Component({ @Component({
selector: 'app-block-health-graph', selector: 'app-block-health-graph',

View file

@ -1,17 +1,17 @@
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core'; import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core';
import { TransactionStripped } from '../../interfaces/node-api.interface'; import { TransactionStripped } from '@interfaces/node-api.interface';
import { FastVertexArray } from './fast-vertex-array'; import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array';
import BlockScene from './block-scene'; import BlockScene from '@components/block-overview-graph/block-scene';
import TxSprite from './tx-sprite'; import TxSprite from '@components/block-overview-graph/tx-sprite';
import TxView from './tx-view'; import TxView from '@components/block-overview-graph/tx-view';
import { Color, Position } from './sprite-types'; import { Color, Position } from '@components/block-overview-graph/sprite-types';
import { Price } from '../../services/price.service'; import { Price } from '@app/services/price.service';
import { StateService } from '../../services/state.service'; import { StateService } from '@app/services/state.service';
import { ThemeService } from '../../services/theme.service'; import { ThemeService } from '@app/services/theme.service';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction, contrastColorFunction, contrastAuditColors, contrastColors } from './utils'; import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction, contrastColorFunction, contrastAuditColors, contrastColors } from '@components/block-overview-graph/utils';
import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils'; import { ActiveFilter, FilterMode, toFlags } from '@app/shared/filters.utils';
import { detectWebGL } from '../../shared/graphs.utils'; import { detectWebGL } from '@app/shared/graphs.utils';
const unmatchedOpacity = 0.2; const unmatchedOpacity = 0.2;
const unmatchedAuditColors = { const unmatchedAuditColors = {

View file

@ -1,9 +1,9 @@
import { FastVertexArray } from './fast-vertex-array'; import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array';
import TxView from './tx-view'; import TxView from '@components/block-overview-graph/tx-view';
import { TransactionStripped } from '../../interfaces/node-api.interface'; import { TransactionStripped } from '@interfaces/node-api.interface';
import { Color, Position, Square, ViewUpdateParams } from './sprite-types'; import { Color, Position, Square, ViewUpdateParams } from '@components/block-overview-graph/sprite-types';
import { defaultColorFunction, contrastColorFunction } from './utils'; import { defaultColorFunction, contrastColorFunction } from '@components/block-overview-graph/utils';
import { ThemeService } from '../../services/theme.service'; import { ThemeService } from '@app/services/theme.service';
export default class BlockScene { export default class BlockScene {
scene: { count: number, offset: { x: number, y: number}}; scene: { count: number, offset: { x: number, y: number}};

View file

@ -8,7 +8,7 @@
or compacting into a smaller Float32Array when there's space to do so. or compacting into a smaller Float32Array when there's space to do so.
*/ */
import TxSprite from './tx-sprite'; import TxSprite from '@components/block-overview-graph/tx-sprite';
export class FastVertexArray { export class FastVertexArray {
length: number; length: number;

View file

@ -1,5 +1,5 @@
import { FastVertexArray } from './fast-vertex-array'; import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array';
import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from './sprite-types'; import { InterpolatedAttribute, Attributes, OptionalAttributes, SpriteUpdateParams, Update } from '@components/block-overview-graph/sprite-types';
const attribKeys = ['a', 'b', 't', 'v']; const attribKeys = ['a', 'b', 't', 'v'];
const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a']; const updateKeys = ['x', 'y', 's', 'r', 'g', 'b', 'a'];

View file

@ -1,10 +1,10 @@
import TxSprite from './tx-sprite'; import TxSprite from '@components/block-overview-graph/tx-sprite';
import { FastVertexArray } from './fast-vertex-array'; import { FastVertexArray } from '@components/block-overview-graph/fast-vertex-array';
import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types'; import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from '@components/block-overview-graph/sprite-types';
import { hexToColor } from './utils'; import { hexToColor } from '@components/block-overview-graph/utils';
import BlockScene from './block-scene'; import BlockScene from '@components/block-overview-graph/block-scene';
import { TransactionStripped } from '../../interfaces/node-api.interface'; import { TransactionStripped } from '@interfaces/node-api.interface';
import { TransactionFlags } from '../../shared/filters.utils'; import { TransactionFlags } from '@app/shared/filters.utils';
const hoverTransitionTime = 300; const hoverTransitionTime = 300;
const defaultHoverColor = hexToColor('1bd8f4'); const defaultHoverColor = hexToColor('1bd8f4');

View file

@ -1,6 +1,6 @@
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../app.constants'; import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '@app/app.constants';
import { Color } from './sprite-types'; import { Color } from '@components/block-overview-graph/sprite-types';
import TxView from './tx-view'; import TxView from '@components/block-overview-graph/tx-view';
export function hexToColor(hex: string): Color { export function hexToColor(hex: string): Color {
return { return {

View file

@ -1,9 +1,9 @@
import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { Component, ElementRef, ViewChild, Input, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Position } from '../../components/block-overview-graph/sprite-types.js'; import { Position } from '@components/block-overview-graph/sprite-types.js';
import { Price } from '../../services/price.service'; import { Price } from '@app/services/price.service';
import { TransactionStripped } from '../../interfaces/node-api.interface.js'; import { TransactionStripped } from '@interfaces/node-api.interface.js';
import { Filter, FilterMode, TransactionFlags, toFilters } from '../../shared/filters.utils'; import { Filter, FilterMode, TransactionFlags, toFilters } from '@app/shared/filters.utils';
import { Block } from '../../interfaces/electrs.interface.js'; import { Block } from '@interfaces/electrs.interface.js';
@Component({ @Component({
selector: 'app-block-overview-tooltip', selector: 'app-block-overview-tooltip',

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