Attempt to merge master into #5376
This commit is contained in:
parent
79e2883ebe
commit
78844f5787
12
LICENSE
12
LICENSE
@ -1,5 +1,5 @@
|
|||||||
The Mempool Open Source Project®
|
The Mempool Open Source Project®
|
||||||
Copyright (c) 2019-2023 Mempool Space K.K. and other shadowy super-coders
|
Copyright (c) 2019-2024 Mempool Space K.K. and other shadowy super-coders
|
||||||
|
|
||||||
This program is free software; you can redistribute it and/or modify it under
|
This program is free software; you can redistribute it and/or modify it under
|
||||||
the terms of the GNU Affero General Public License as published by the Free
|
the terms of the GNU Affero General Public License as published by the Free
|
||||||
@ -12,10 +12,12 @@ or any other contributor to The Mempool Open Source Project.
|
|||||||
|
|
||||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
|
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®,
|
||||||
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
|
Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full
|
||||||
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square logo,
|
Bitcoin ecosystem™, Mempool Goggles™, the mempool Logo, the mempool Square Logo,
|
||||||
the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical
|
the mempool block visualization Logo, the mempool Blocks Logo, the mempool
|
||||||
Logo, and the mempool.space Horizontal logo are registered trademarks or trademarks
|
transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo,
|
||||||
of Mempool Space K.K in Japan, the United States, and/or other countries.
|
the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are
|
||||||
|
registered trademarks or trademarks of Mempool Space K.K in Japan,
|
||||||
|
the United States, and/or other countries.
|
||||||
|
|
||||||
See our full Trademark Policy and Guidelines for more details, published on
|
See our full Trademark Policy and Guidelines for more details, published on
|
||||||
<https://mempool.space/trademark-policy>.
|
<https://mempool.space/trademark-policy>.
|
||||||
|
|||||||
@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
|
|||||||
|
|
||||||
#### Build
|
#### Build
|
||||||
|
|
||||||
_Make sure to use Node.js 16.10 and npm 7._
|
_Make sure to use Node.js 20.x and npm 9.x or newer_
|
||||||
|
|
||||||
_The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._
|
_The build process requires [Rust](https://www.rust-lang.org/tools/install) to be installed._
|
||||||
|
|
||||||
|
|||||||
@ -27,8 +27,9 @@
|
|||||||
"AUTOMATIC_POOLS_UPDATE": false,
|
"AUTOMATIC_POOLS_UPDATE": false,
|
||||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
|
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
|
||||||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||||
|
"POOLS_UPDATE_DELAY": 604800,
|
||||||
"AUDIT": false,
|
"AUDIT": false,
|
||||||
"RUST_GBT": false,
|
"RUST_GBT": true,
|
||||||
"LIMIT_GBT": false,
|
"LIMIT_GBT": false,
|
||||||
"CPFP_INDEXING": false,
|
"CPFP_INDEXING": false,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||||
@ -45,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",
|
||||||
|
|||||||
947
backend/package-lock.json
generated
947
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-backend",
|
"name": "mempool-backend",
|
||||||
"version": "3.0.0-dev",
|
"version": "3.1.0-dev",
|
||||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"homepage": "https://mempool.space",
|
"homepage": "https://mempool.space",
|
||||||
@ -39,24 +39,24 @@
|
|||||||
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
|
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.24.0",
|
"@babel/core": "^7.25.2",
|
||||||
"@mempool/electrum-client": "1.1.9",
|
"@mempool/electrum-client": "1.1.9",
|
||||||
"@types/node": "^18.15.3",
|
"@types/node": "^18.15.3",
|
||||||
"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.19.2",
|
"express": "~4.21.1",
|
||||||
"maxmind": "~4.3.11",
|
"maxmind": "~4.3.11",
|
||||||
"mysql2": "~3.10.0",
|
"mysql2": "~3.11.0",
|
||||||
"rust-gbt": "file:./rust-gbt",
|
"rust-gbt": "file:./rust-gbt",
|
||||||
"redis": "^4.6.6",
|
"redis": "^4.7.0",
|
||||||
"socks-proxy-agent": "~7.0.0",
|
"socks-proxy-agent": "~7.0.0",
|
||||||
"typescript": "~4.9.3",
|
"typescript": "~4.9.3",
|
||||||
"ws": "~8.18.0"
|
"ws": "~8.18.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/code-frame": "^7.18.6",
|
"@babel/code-frame": "^7.18.6",
|
||||||
"@babel/core": "^7.24.0",
|
"@babel/core": "^7.25.2",
|
||||||
"@types/compression": "^1.7.2",
|
"@types/compression": "^1.7.2",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
|
|||||||
@ -28,6 +28,7 @@
|
|||||||
"INDEXING_BLOCKS_AMOUNT": 14,
|
"INDEXING_BLOCKS_AMOUNT": 14,
|
||||||
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
||||||
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
|
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
|
||||||
|
"POOLS_UPDATE_DELAY": 604800,
|
||||||
"AUDIT": true,
|
"AUDIT": true,
|
||||||
"RUST_GBT": false,
|
"RUST_GBT": false,
|
||||||
"LIMIT_GBT": false,
|
"LIMIT_GBT": false,
|
||||||
@ -46,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__",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Common } from '../../api/common';
|
import { Common } from '../../api/common';
|
||||||
import { MempoolTransactionExtended } from '../../mempool.interfaces';
|
import { MempoolTransactionExtended, TransactionExtended } from '../../mempool.interfaces';
|
||||||
|
|
||||||
const randomTransactions = require('./test-data/transactions-random.json');
|
const randomTransactions = require('./test-data/transactions-random.json');
|
||||||
const replacedTransactions = require('./test-data/transactions-replaced.json');
|
const replacedTransactions = require('./test-data/transactions-replaced.json');
|
||||||
@ -10,14 +10,14 @@ describe('Common', () => {
|
|||||||
describe('RBF', () => {
|
describe('RBF', () => {
|
||||||
const newTransactions = rbfTransactions.concat(randomTransactions);
|
const newTransactions = rbfTransactions.concat(randomTransactions);
|
||||||
test('should detect RBF transactions with fast method', () => {
|
test('should detect RBF transactions with fast method', () => {
|
||||||
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions);
|
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions);
|
||||||
expect(Object.values(result).length).toEqual(2);
|
expect(Object.values(result).length).toEqual(2);
|
||||||
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should detect RBF transactions with scalable method', () => {
|
test('should detect RBF transactions with scalable method', () => {
|
||||||
const result: { [txid: string]: MempoolTransactionExtended[] } = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
|
const result: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = Common.findRbfTransactions(newTransactions, replacedTransactions, true);
|
||||||
expect(Object.values(result).length).toEqual(2);
|
expect(Object.values(result).length).toEqual(2);
|
||||||
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
expect(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||||
|
|||||||
@ -41,8 +41,9 @@ describe('Mempool Backend Config', () => {
|
|||||||
STDOUT_LOG_MIN_PRIORITY: 'debug',
|
STDOUT_LOG_MIN_PRIORITY: 'debug',
|
||||||
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
POOLS_JSON_TREE_URL: 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||||
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
POOLS_JSON_URL: 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
||||||
|
POOLS_UPDATE_DELAY: 604800,
|
||||||
AUDIT: false,
|
AUDIT: false,
|
||||||
RUST_GBT: false,
|
RUST_GBT: true,
|
||||||
LIMIT_GBT: false,
|
LIMIT_GBT: false,
|
||||||
CPFP_INDEXING: false,
|
CPFP_INDEXING: false,
|
||||||
MAX_BLOCKS_BULK_QUERY: 0,
|
MAX_BLOCKS_BULK_QUERY: 0,
|
||||||
@ -73,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({
|
||||||
|
|||||||
@ -70,7 +70,7 @@ class AboutRoutes {
|
|||||||
res.status(500).end();
|
res.status(500).end();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username', async (req, res) => {
|
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username/:md5', async (req, res) => {
|
||||||
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
|
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
|
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
|
||||||
|
|||||||
@ -2,24 +2,28 @@ import config from '../config';
|
|||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||||
import rbfCache from './rbf-cache';
|
import rbfCache from './rbf-cache';
|
||||||
|
import transactionUtils from './transaction-utils';
|
||||||
|
|
||||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||||
|
|
||||||
class Audit {
|
class Audit {
|
||||||
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
|
auditBlock(height: number, transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended })
|
||||||
: { censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
|
: { unseen: string[], censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
|
||||||
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||||
return { censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
|
return { unseen: [], censored: [], added: [], prioritized: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 1, similarity: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const matches: string[] = []; // present in both mined block and template
|
const matches: string[] = []; // present in both mined block and template
|
||||||
const added: string[] = []; // present in mined block, not in template
|
const added: string[] = []; // present in mined block, not in template
|
||||||
const prioritized: string[] = [] // present in the mined block, not in the template, but further down in the mempool
|
const unseen: string[] = []; // present in the mined block, not in our mempool
|
||||||
|
let prioritized: string[] = []; // higher in the block than would be expected by in-band feerate alone
|
||||||
|
let deprioritized: string[] = []; // lower in the block than would be expected by in-band feerate alone
|
||||||
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
||||||
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
||||||
const accelerated: string[] = []; // prioritized by the mempool accelerator
|
const accelerated: string[] = []; // prioritized by the mempool accelerator
|
||||||
const isCensored = {}; // missing, without excuse
|
const isCensored = {}; // missing, without excuse
|
||||||
const isDisplaced = {};
|
const isDisplaced = {};
|
||||||
|
const isAccelerated = {};
|
||||||
let displacedWeight = 0;
|
let displacedWeight = 0;
|
||||||
let matchedWeight = 0;
|
let matchedWeight = 0;
|
||||||
let projectedWeight = 0;
|
let projectedWeight = 0;
|
||||||
@ -32,6 +36,7 @@ class Audit {
|
|||||||
inBlock[tx.txid] = tx;
|
inBlock[tx.txid] = tx;
|
||||||
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
|
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
|
||||||
accelerated.push(tx.txid);
|
accelerated.push(tx.txid);
|
||||||
|
isAccelerated[tx.txid] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// coinbase is always expected
|
// coinbase is always expected
|
||||||
@ -113,11 +118,16 @@ class Audit {
|
|||||||
} else {
|
} else {
|
||||||
if (rbfCache.has(tx.txid)) {
|
if (rbfCache.has(tx.txid)) {
|
||||||
rbf.push(tx.txid);
|
rbf.push(tx.txid);
|
||||||
} else if (!isDisplaced[tx.txid]) {
|
if (!mempool[tx.txid] && !rbfCache.getReplacedBy(tx.txid)) {
|
||||||
|
unseen.push(tx.txid);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (mempool[tx.txid]) {
|
if (mempool[tx.txid]) {
|
||||||
prioritized.push(tx.txid);
|
if (isDisplaced[tx.txid]) {
|
||||||
|
added.push(tx.txid);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
added.push(tx.txid);
|
unseen.push(tx.txid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
overflowWeight += tx.weight;
|
overflowWeight += tx.weight;
|
||||||
@ -125,6 +135,8 @@ class Audit {
|
|||||||
totalWeight += tx.weight;
|
totalWeight += tx.weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize'));
|
||||||
|
|
||||||
// transactions missing from near the end of our template are probably not being censored
|
// transactions missing from near the end of our template are probably not being censored
|
||||||
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
|
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
|
||||||
let maxOverflowRate = 0;
|
let maxOverflowRate = 0;
|
||||||
@ -165,6 +177,7 @@ class Audit {
|
|||||||
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
|
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
unseen,
|
||||||
censored: Object.keys(isCensored),
|
censored: Object.keys(isCensored),
|
||||||
added,
|
added,
|
||||||
prioritized,
|
prioritized,
|
||||||
|
|||||||
@ -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,6 +23,7 @@ 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[][]>;
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
@ -327,6 +331,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
'witness_v1_taproot': 'v1_p2tr',
|
'witness_v1_taproot': 'v1_p2tr',
|
||||||
'nonstandard': 'nonstandard',
|
'nonstandard': 'nonstandard',
|
||||||
'multisig': 'multisig',
|
'multisig': 'multisig',
|
||||||
|
'anchor': 'anchor',
|
||||||
'nulldata': 'op_return'
|
'nulldata': 'op_return'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define a set of routes used by the accelerator server
|
* Define a set of routes used by the accelerator server
|
||||||
@ -11,15 +12,15 @@ class BitcoinBackendRoutes {
|
|||||||
|
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application) {
|
||||||
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)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import difficultyAdjustment from '../difficulty-adjustment';
|
|||||||
import transactionRepository from '../../repositories/TransactionRepository';
|
import transactionRepository from '../../repositories/TransactionRepository';
|
||||||
import rbfCache from '../rbf-cache';
|
import rbfCache from '../rbf-cache';
|
||||||
import { calculateMempoolTxCpfp } from '../cpfp';
|
import { calculateMempoolTxCpfp } from '../cpfp';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class BitcoinRoutes {
|
class BitcoinRoutes {
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application) {
|
||||||
@ -41,12 +42,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') {
|
||||||
@ -86,7 +90,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,13 +109,13 @@ class BitcoinRoutes {
|
|||||||
const result = mempoolBlocks.getMempoolBlocks();
|
const result = mempoolBlocks.getMempoolBlocks();
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTransactionTimes(req: Request, res: Response) {
|
private getTransactionTimes(req: Request, res: Response) {
|
||||||
if (!Array.isArray(req.query.txId)) {
|
if (!Array.isArray(req.query.txId)) {
|
||||||
res.status(500).send('Not an array');
|
handleError(req, res, 500, 'Not an array');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const txIds: string[] = [];
|
const txIds: string[] = [];
|
||||||
@ -128,12 +132,12 @@ class BitcoinRoutes {
|
|||||||
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
|
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
|
||||||
const txids_csv = req.query.txids;
|
const txids_csv = req.query.txids;
|
||||||
if (!txids_csv || typeof txids_csv !== 'string') {
|
if (!txids_csv || typeof txids_csv !== 'string') {
|
||||||
res.status(500).send('Invalid txids format');
|
handleError(req, res, 500, 'Invalid txids format');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const txids = txids_csv.split(',');
|
const txids = txids_csv.split(',');
|
||||||
if (txids.length > 50) {
|
if (txids.length > 50) {
|
||||||
res.status(400).send('Too many txids requested');
|
handleError(req, res, 400, 'Too many txids requested');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,13 +145,13 @@ class BitcoinRoutes {
|
|||||||
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
|
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
|
||||||
res.json(batchedOutspends);
|
res.json(batchedOutspends);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) {
|
||||||
res.status(501).send(`Invalid transaction ID.`);
|
handleError(req, res, 501, `Invalid transaction ID.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,6 +169,7 @@ class BitcoinRoutes {
|
|||||||
acceleration: tx.acceleration,
|
acceleration: tx.acceleration,
|
||||||
acceleratedBy: tx.acceleratedBy || undefined,
|
acceleratedBy: tx.acceleratedBy || undefined,
|
||||||
acceleratedAt: tx.acceleratedAt || undefined,
|
acceleratedAt: tx.acceleratedAt || undefined,
|
||||||
|
feeDelta: tx.feeDelta || undefined,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -179,7 +184,7 @@ class BitcoinRoutes {
|
|||||||
try {
|
try {
|
||||||
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
|
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send('failed to get CPFP info');
|
handleError(req, res, 500, 'failed to get CPFP info');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,7 +213,7 @@ class BitcoinRoutes {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,7 +227,7 @@ class BitcoinRoutes {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,13 +288,13 @@ class BitcoinRoutes {
|
|||||||
// Not modified
|
// Not modified
|
||||||
// 422 Unprocessable Entity
|
// 422 Unprocessable Entity
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422
|
||||||
res.status(422).send(`Psbt had no missing nonWitnessUtxos.`);
|
handleError(req, res, 422, `Psbt had no missing nonWitnessUtxos.`);
|
||||||
}
|
}
|
||||||
} 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)) {
|
||||||
res.status(404).send(e.message);
|
handleError(req, res, 404, e.message);
|
||||||
} else {
|
} else {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -303,7 +308,7 @@ class BitcoinRoutes {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
res.status(statusCode).send(e instanceof Error ? e.message : e);
|
handleError(req, res, statusCode, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,6 +317,20 @@ class BitcoinRoutes {
|
|||||||
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) {
|
||||||
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getStrippedBlockTransaction(req: Request, res: Response) {
|
||||||
|
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) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
@ -335,7 +354,7 @@ class BitcoinRoutes {
|
|||||||
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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -345,7 +364,7 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('content-type', 'text/plain');
|
res.setHeader('content-type', 'text/plain');
|
||||||
res.send(blockHeader);
|
res.send(blockHeader);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,10 +375,11 @@ class BitcoinRoutes {
|
|||||||
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 {
|
||||||
return res.status(404).send(`audit not available`);
|
handleError(req, res, 404, `audit not available`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,7 +390,8 @@ class BitcoinRoutes {
|
|||||||
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 {
|
||||||
return res.status(404).send(`transaction audit not available`);
|
handleError(req, res, 404, `transaction audit not available`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
@ -387,42 +408,49 @@ class BitcoinRoutes {
|
|||||||
return await this.getLegacyBlocks(req, res);
|
return await this.getLegacyBlocks(req, res);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getBlocksByBulk(req: Request, res: Response) {
|
private async getBlocksByBulk(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false) { // Liquid - Not implemented
|
||||||
return res.status(404).send(`This API is only available for Bitcoin networks`);
|
handleError(req, res, 404, `This API is only available for Bitcoin networks`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
|
if (config.MEMPOOL.MAX_BLOCKS_BULK_QUERY <= 0) {
|
||||||
return res.status(404).send(`This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
|
handleError(req, res, 404, `This API is disabled. Set config.MEMPOOL.MAX_BLOCKS_BULK_QUERY to a positive number to enable it.`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (!Common.indexingEnabled()) {
|
if (!Common.indexingEnabled()) {
|
||||||
return res.status(404).send(`Indexing is required for this API`);
|
handleError(req, res, 404, `Indexing is required for this API`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const from = parseInt(req.params.from, 10);
|
const from = parseInt(req.params.from, 10);
|
||||||
if (!req.params.from || from < 0) {
|
if (!req.params.from || from < 0) {
|
||||||
return res.status(400).send(`Parameter 'from' must be a block height (integer)`);
|
handleError(req, res, 400, `Parameter 'from' must be a block height (integer)`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
|
const to = req.params.to === undefined ? await bitcoinApi.$getBlockHeightTip() : parseInt(req.params.to, 10);
|
||||||
if (to < 0) {
|
if (to < 0) {
|
||||||
return res.status(400).send(`Parameter 'to' must be a block height (integer)`);
|
handleError(req, res, 400, `Parameter 'to' must be a block height (integer)`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (from > to) {
|
if (from > to) {
|
||||||
return res.status(400).send(`Parameter 'to' must be a higher block height than 'from'`);
|
handleError(req, res, 400, `Parameter 'to' must be a higher block height than 'from'`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) {
|
if ((to - from + 1) > config.MEMPOOL.MAX_BLOCKS_BULK_QUERY) {
|
||||||
return res.status(400).send(`You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
|
handleError(req, res, 400, `You can only query ${config.MEMPOOL.MAX_BLOCKS_BULK_QUERY} blocks at once.`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
res.json(await blocks.$getBlocksBetweenHeight(from, to));
|
res.json(await blocks.$getBlocksBetweenHeight(from, to));
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -457,10 +485,10 @@ 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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getBlockTransactions(req: Request, res: Response) {
|
private async getBlockTransactions(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
|
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
|
||||||
@ -482,7 +510,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);
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,13 +519,13 @@ 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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAddress(req: Request, res: Response) {
|
private async getAddress(req: Request, res: Response) {
|
||||||
if (config.MEMPOOL.BACKEND === 'none') {
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -506,15 +534,16 @@ 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)) {
|
||||||
return res.status(413).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAddressTransactions(req: Request, res: Response): Promise<void> {
|
private async getAddressTransactions(req: Request, res: Response): Promise<void> {
|
||||||
if (config.MEMPOOL.BACKEND === 'none') {
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -527,23 +556,23 @@ 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)) {
|
||||||
res.status(413).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
|
private async getAddressTransactionSummary(req: Request, res: Response): Promise<void> {
|
||||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
res.status(405).send('Address summary lookups require mempool/electrs backend.');
|
handleError(req, res, 405, 'Address summary lookups require mempool/electrs backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getScriptHash(req: Request, res: Response) {
|
private async getScriptHash(req: Request, res: Response) {
|
||||||
if (config.MEMPOOL.BACKEND === 'none') {
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,15 +583,16 @@ 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)) {
|
||||||
return res.status(413).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
|
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
|
||||||
if (config.MEMPOOL.BACKEND === 'none') {
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
handleError(req, res, 405, 'Address lookups cannot be used with bitcoind as backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -577,16 +607,16 @@ 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)) {
|
||||||
res.status(413).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 413, e instanceof Error ? e.message : e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
|
private async getScriptHashTransactionSummary(req: Request, res: Response): Promise<void> {
|
||||||
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
if (config.MEMPOOL.BACKEND !== 'esplora') {
|
||||||
res.status(405).send('Scripthash summary lookups require mempool/electrs backend.');
|
handleError(req, res, 405, 'Scripthash summary lookups require mempool/electrs backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -596,7 +626,7 @@ class BitcoinRoutes {
|
|||||||
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
||||||
res.send(blockHash);
|
res.send(blockHash);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -623,7 +653,7 @@ class BitcoinRoutes {
|
|||||||
const rawMempool = await bitcoinApi.$getRawMempool();
|
const rawMempool = await bitcoinApi.$getRawMempool();
|
||||||
res.send(rawMempool);
|
res.send(rawMempool);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -631,12 +661,13 @@ class BitcoinRoutes {
|
|||||||
try {
|
try {
|
||||||
const result = blocks.getCurrentBlockHeight();
|
const result = blocks.getCurrentBlockHeight();
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return res.status(503).send(`Service Temporarily Unavailable`);
|
handleError(req, res, 503, `Service Temporarily Unavailable`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
res.setHeader('content-type', 'text/plain');
|
res.setHeader('content-type', 'text/plain');
|
||||||
res.send(result.toString());
|
res.send(result.toString());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -646,7 +677,7 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('content-type', 'text/plain');
|
res.setHeader('content-type', 'text/plain');
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -656,7 +687,7 @@ class BitcoinRoutes {
|
|||||||
res.setHeader('content-type', 'application/octet-stream');
|
res.setHeader('content-type', 'application/octet-stream');
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -665,7 +696,7 @@ class BitcoinRoutes {
|
|||||||
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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -674,7 +705,7 @@ class BitcoinRoutes {
|
|||||||
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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -687,7 +718,7 @@ class BitcoinRoutes {
|
|||||||
replaces
|
replaces
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -696,7 +727,7 @@ class BitcoinRoutes {
|
|||||||
const result = rbfCache.getRbfTrees(false);
|
const result = rbfCache.getRbfTrees(false);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -705,7 +736,7 @@ class BitcoinRoutes {
|
|||||||
const result = rbfCache.getRbfTrees(true);
|
const result = rbfCache.getRbfTrees(true);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -718,7 +749,7 @@ class BitcoinRoutes {
|
|||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -727,7 +758,7 @@ class BitcoinRoutes {
|
|||||||
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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -737,10 +768,10 @@ class BitcoinRoutes {
|
|||||||
if (da) {
|
if (da) {
|
||||||
res.json(da);
|
res.json(da);
|
||||||
} else {
|
} else {
|
||||||
res.status(503).send(`Service Temporarily Unavailable`);
|
handleError(req, res, 503, `Service Temporarily Unavailable`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -751,7 +782,7 @@ 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) {
|
||||||
res.status(400).send(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, message: e.message })
|
||||||
: (e.message || 'Error'));
|
: (e.message || 'Error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -763,7 +794,7 @@ 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) {
|
||||||
res.status(400).send(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, message: e.message })
|
||||||
: (e.message || 'Error'));
|
: (e.message || 'Error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -775,8 +806,20 @@ 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) {
|
||||||
res.setHeader('content-type', 'text/plain');
|
handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||||
res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
: (e.message || 'Error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, message: e.message })
|
||||||
: (e.message || 'Error'));
|
: (e.message || 'Error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact
|
|||||||
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';
|
||||||
|
|
||||||
interface FailoverHost {
|
interface FailoverHost {
|
||||||
host: string,
|
host: string,
|
||||||
@ -305,7 +305,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 +332,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,8 @@ import AccelerationRepository from '../repositories/AccelerationRepository';
|
|||||||
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
|
import { calculateFastBlockCpfp, calculateGoodBlockCpfp } from './cpfp';
|
||||||
import mempool from './mempool';
|
import mempool from './mempool';
|
||||||
import CpfpRepository from '../repositories/CpfpRepository';
|
import CpfpRepository from '../repositories/CpfpRepository';
|
||||||
|
import accelerationApi from './services/acceleration';
|
||||||
|
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private blocks: BlockExtended[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
@ -218,10 +220,10 @@ class Blocks {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public summarizeBlockTransactions(hash: string, transactions: TransactionExtended[]): BlockSummary {
|
public summarizeBlockTransactions(hash: string, height: number, transactions: TransactionExtended[]): BlockSummary {
|
||||||
return {
|
return {
|
||||||
id: hash,
|
id: hash,
|
||||||
transactions: Common.classifyTransactions(transactions),
|
transactions: Common.classifyTransactions(transactions, height),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,7 +343,12 @@ class Blocks {
|
|||||||
id: pool.uniqueId,
|
id: pool.uniqueId,
|
||||||
name: pool.name,
|
name: pool.name,
|
||||||
slug: pool.slug,
|
slug: pool.slug,
|
||||||
|
minerNames: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (extras.pool.name === 'OCEAN') {
|
||||||
|
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extras.matchRate = null;
|
extras.matchRate = null;
|
||||||
@ -405,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
|
||||||
@ -439,7 +454,7 @@ class Blocks {
|
|||||||
|
|
||||||
|
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
const txs = (await bitcoinApi.$getTxsForBlock(block.hash)).map(tx => transactionUtils.extendMempoolTransaction(tx));
|
||||||
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
|
const cpfpSummary = await this.$indexCPFP(block.hash, block.height, txs);
|
||||||
if (cpfpSummary) {
|
if (cpfpSummary) {
|
||||||
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
|
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
|
||||||
@ -615,7 +630,7 @@ class Blocks {
|
|||||||
// add CPFP
|
// add CPFP
|
||||||
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
|
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
|
||||||
// classify
|
// classify
|
||||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
|
||||||
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
|
await BlocksSummariesRepository.$saveTransactions(height, blockHash, classifiedTxs, 2);
|
||||||
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
|
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
|
||||||
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
|
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
|
||||||
@ -652,7 +667,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
|
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
|
||||||
// classify
|
// classify
|
||||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
|
||||||
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
|
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
|
||||||
for (const tx of classifiedTxs) {
|
for (const tx of classifiedTxs) {
|
||||||
classifiedTxMap[tx.txid] = tx;
|
classifiedTxMap[tx.txid] = tx;
|
||||||
@ -904,9 +919,14 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, Object.values(mempool.getAccelerations()).map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
|
let accelerations = Object.values(mempool.getAccelerations());
|
||||||
|
if (accelerations?.length > 0) {
|
||||||
|
const pool = await this.$findBlockMiner(transactionUtils.stripCoinbaseTransaction(transactions[0]));
|
||||||
|
accelerations = accelerations.filter(a => a.pools.includes(pool.uniqueId));
|
||||||
|
}
|
||||||
|
const cpfpSummary: CpfpSummary = calculateGoodBlockCpfp(block.height, transactions, accelerations.map(a => ({ txid: a.txid, max_bid: a.feeDelta })));
|
||||||
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
const blockExtended: BlockExtended = await this.$getBlockExtended(block, cpfpSummary.transactions);
|
||||||
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, cpfpSummary.transactions);
|
const blockSummary: BlockSummary = this.summarizeBlockTransactions(block.id, block.height, cpfpSummary.transactions);
|
||||||
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
|
this.updateTimerProgress(timer, `got block data for ${this.currentBlockHeight}`);
|
||||||
|
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled()) {
|
||||||
@ -927,12 +947,12 @@ class Blocks {
|
|||||||
const newBlock = await this.$indexBlock(lastBlock.height - i);
|
const newBlock = await this.$indexBlock(lastBlock.height - i);
|
||||||
this.blocks.push(newBlock);
|
this.blocks.push(newBlock);
|
||||||
this.updateTimerProgress(timer, `reindexed block`);
|
this.updateTimerProgress(timer, `reindexed block`);
|
||||||
let cpfpSummary;
|
let newCpfpSummary;
|
||||||
if (config.MEMPOOL.CPFP_INDEXING) {
|
if (config.MEMPOOL.CPFP_INDEXING) {
|
||||||
cpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
|
newCpfpSummary = await this.$indexCPFP(newBlock.id, lastBlock.height - i);
|
||||||
this.updateTimerProgress(timer, `reindexed block cpfp`);
|
this.updateTimerProgress(timer, `reindexed block cpfp`);
|
||||||
}
|
}
|
||||||
await this.$getStrippedBlockTransactions(newBlock.id, true, true, cpfpSummary, newBlock.height);
|
await this.$getStrippedBlockTransactions(newBlock.id, true, true, newCpfpSummary, newBlock.height);
|
||||||
this.updateTimerProgress(timer, `reindexed block summary`);
|
this.updateTimerProgress(timer, `reindexed block summary`);
|
||||||
}
|
}
|
||||||
await mining.$indexDifficultyAdjustments();
|
await mining.$indexDifficultyAdjustments();
|
||||||
@ -981,7 +1001,7 @@ class Blocks {
|
|||||||
|
|
||||||
// start async callbacks
|
// start async callbacks
|
||||||
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
|
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
|
||||||
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
|
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, cpfpSummary.transactions));
|
||||||
|
|
||||||
if (block.height % 2016 === 0) {
|
if (block.height % 2016 === 0) {
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled()) {
|
||||||
@ -1163,7 +1183,7 @@ class Blocks {
|
|||||||
transactions: cpfpSummary.transactions.map(tx => {
|
transactions: cpfpSummary.transactions.map(tx => {
|
||||||
let flags: number = 0;
|
let flags: number = 0;
|
||||||
try {
|
try {
|
||||||
flags = Common.getTransactionFlags(tx);
|
flags = Common.getTransactionFlags(tx, height);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
|
logger.warn('Failed to classify transaction: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
@ -1178,11 +1198,11 @@ class Blocks {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
summaryVersion = 1;
|
summaryVersion = cpfpSummary.version;
|
||||||
} else {
|
} else {
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
const txs = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||||
summary = this.summarizeBlockTransactions(hash, txs);
|
summary = this.summarizeBlockTransactions(hash, height || 0, txs);
|
||||||
summaryVersion = 1;
|
summaryVersion = 1;
|
||||||
} else {
|
} else {
|
||||||
// Call Core RPC
|
// Call Core RPC
|
||||||
@ -1204,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
|
||||||
*
|
*
|
||||||
@ -1318,7 +1343,7 @@ class Blocks {
|
|||||||
let summaryVersion = 0;
|
let summaryVersion = 0;
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
const txs = (await bitcoinApi.$getTxsForBlock(cleanBlock.hash)).map(tx => transactionUtils.extendTransaction(tx));
|
||||||
summary = this.summarizeBlockTransactions(cleanBlock.hash, txs);
|
summary = this.summarizeBlockTransactions(cleanBlock.hash, cleanBlock.height, txs);
|
||||||
summaryVersion = 1;
|
summaryVersion = 1;
|
||||||
} else {
|
} else {
|
||||||
// Call Core RPC
|
// Call Core RPC
|
||||||
@ -1397,11 +1422,11 @@ class Blocks {
|
|||||||
return this.currentBlockHeight;
|
return this.currentBlockHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $indexCPFP(hash: string, height: number, txs?: TransactionExtended[]): Promise<CpfpSummary | null> {
|
public async $indexCPFP(hash: string, height: number, txs?: MempoolTransactionExtended[]): Promise<CpfpSummary | null> {
|
||||||
let transactions = txs;
|
let transactions = txs;
|
||||||
if (!transactions) {
|
if (!transactions) {
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendTransaction(tx));
|
transactions = (await bitcoinApi.$getTxsForBlock(hash)).map(tx => transactionUtils.extendMempoolTransaction(tx));
|
||||||
}
|
}
|
||||||
if (!transactions) {
|
if (!transactions) {
|
||||||
const block = await bitcoinClient.getBlock(hash, 2);
|
const block = await bitcoinClient.getBlock(hash, 2);
|
||||||
@ -1413,7 +1438,7 @@ class Blocks {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (transactions?.length != null) {
|
if (transactions?.length != null) {
|
||||||
const summary = calculateFastBlockCpfp(height, transactions as TransactionExtended[]);
|
const summary = calculateFastBlockCpfp(height, transactions);
|
||||||
|
|
||||||
await this.$saveCpfp(hash, height, summary);
|
await this.$saveCpfp(hash, height, summary);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { CpfpInfo, CpfpSummary, CpfpCluster, EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
|
import { EffectiveFeeStats, MempoolBlockWithTransactions, TransactionExtended, MempoolTransactionExtended, TransactionStripped, WorkingEffectiveFeeStats, TransactionClassified, TransactionFlags } from '../mempool.interfaces';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
import { NodeSocket } from '../repositories/NodesSocketsRepository';
|
||||||
import { isIP } from 'net';
|
import { isIP } from 'net';
|
||||||
@ -10,7 +10,6 @@ import logger from '../logger';
|
|||||||
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
|
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
|
||||||
|
|
||||||
// Bitcoin Core default policy settings
|
// Bitcoin Core default policy settings
|
||||||
const TX_MAX_STANDARD_VERSION = 2;
|
|
||||||
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
||||||
const MAX_BLOCK_SIGOPS_COST = 80_000;
|
const MAX_BLOCK_SIGOPS_COST = 80_000;
|
||||||
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
|
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
|
||||||
@ -80,8 +79,8 @@ export class Common {
|
|||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
|
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
|
||||||
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
|
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
|
||||||
|
|
||||||
// For small N, a naive nested loop is extremely fast, but it doesn't scale
|
// For small N, a naive nested loop is extremely fast, but it doesn't scale
|
||||||
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
|
if (added.length < 1000 && deleted.length < 50 && !forceScalable) {
|
||||||
@ -96,7 +95,7 @@ export class Common {
|
|||||||
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
addedTx.vin.some((vin) => vin.txid === deletedVin.txid && vin.vout === deletedVin.vout));
|
||||||
});
|
});
|
||||||
if (foundMatches?.length) {
|
if (foundMatches?.length) {
|
||||||
matches[addedTx.txid] = [...new Set(foundMatches)];
|
matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -124,7 +123,7 @@ export class Common {
|
|||||||
foundMatches.add(deletedTx);
|
foundMatches.add(deletedTx);
|
||||||
}
|
}
|
||||||
if (foundMatches.size) {
|
if (foundMatches.size) {
|
||||||
matches[addedTx.txid] = [...foundMatches];
|
matches[addedTx.txid] = { replaced: [...foundMatches], replacedBy: addedTx };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,17 +138,17 @@ export class Common {
|
|||||||
const replaced: Set<MempoolTransactionExtended> = new Set();
|
const replaced: Set<MempoolTransactionExtended> = new Set();
|
||||||
for (let i = 0; i < tx.vin.length; i++) {
|
for (let i = 0; i < tx.vin.length; i++) {
|
||||||
const vin = tx.vin[i];
|
const vin = tx.vin[i];
|
||||||
const match = spendMap.get(`${vin.txid}:${vin.vout}`);
|
const key = `${vin.txid}:${vin.vout}`;
|
||||||
|
const match = spendMap.get(key);
|
||||||
if (match && match.txid !== tx.txid) {
|
if (match && match.txid !== tx.txid) {
|
||||||
replaced.add(match);
|
replaced.add(match);
|
||||||
// remove this tx from the spendMap
|
// remove this tx from the spendMap
|
||||||
// prevents the same tx being replaced more than once
|
// prevents the same tx being replaced more than once
|
||||||
for (const replacedVin of match.vin) {
|
for (const replacedVin of match.vin) {
|
||||||
const key = `${replacedVin.txid}:${replacedVin.vout}`;
|
const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`;
|
||||||
spendMap.delete(key);
|
spendMap.delete(replacedKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const key = `${vin.txid}:${vin.vout}`;
|
|
||||||
spendMap.delete(key);
|
spendMap.delete(key);
|
||||||
}
|
}
|
||||||
if (replaced.size) {
|
if (replaced.size) {
|
||||||
@ -200,10 +199,13 @@ export class Common {
|
|||||||
*
|
*
|
||||||
* returns true early if any standardness rule is violated, otherwise false
|
* returns true early if any standardness rule is violated, otherwise false
|
||||||
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
|
* (except for non-mandatory-script-verify-flag and p2sh script evaluation rules which are *not* enforced)
|
||||||
|
*
|
||||||
|
* As standardness rules change, we'll need to apply the rules in force *at the time* to older blocks.
|
||||||
|
* For now, just pull out individual rules into versioned functions where necessary.
|
||||||
*/
|
*/
|
||||||
static isNonStandard(tx: TransactionExtended): boolean {
|
static isNonStandard(tx: TransactionExtended, height?: number): boolean {
|
||||||
// version
|
// version
|
||||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
if (this.isNonStandardVersion(tx, height)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,6 +252,8 @@ export class Common {
|
|||||||
}
|
}
|
||||||
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
|
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
|
||||||
return true;
|
return true;
|
||||||
|
} else if (this.isNonStandardAnchor(tx, height)) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
// TODO: bad-witness-nonstandard
|
// TODO: bad-witness-nonstandard
|
||||||
}
|
}
|
||||||
@ -335,6 +339,49 @@ export class Common {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Individual versioned standardness rules
|
||||||
|
|
||||||
|
static V3_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||||
|
'testnet4': 42_000,
|
||||||
|
'testnet': 2_900_000,
|
||||||
|
'signet': 211_000,
|
||||||
|
'': 863_500,
|
||||||
|
};
|
||||||
|
static isNonStandardVersion(tx: TransactionExtended, height?: number): boolean {
|
||||||
|
let TX_MAX_STANDARD_VERSION = 3;
|
||||||
|
if (
|
||||||
|
height != null
|
||||||
|
&& this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||||
|
&& height <= this.V3_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||||
|
) {
|
||||||
|
// V3 transactions were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||||
|
TX_MAX_STANDARD_VERSION = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT = {
|
||||||
|
'testnet4': 42_000,
|
||||||
|
'testnet': 2_900_000,
|
||||||
|
'signet': 211_000,
|
||||||
|
'': 863_500,
|
||||||
|
};
|
||||||
|
static isNonStandardAnchor(tx: TransactionExtended, height?: number): boolean {
|
||||||
|
if (
|
||||||
|
height != null
|
||||||
|
&& this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||||
|
&& height <= this.ANCHOR_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
|
||||||
|
) {
|
||||||
|
// anchor outputs were non-standard to spend before v28.x (scheduled for 2024/09/30 https://github.com/bitcoin/bitcoin/issues/29891)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
static getNonWitnessSize(tx: TransactionExtended): number {
|
static getNonWitnessSize(tx: TransactionExtended): number {
|
||||||
let weight = tx.weight;
|
let weight = tx.weight;
|
||||||
let hasWitness = false;
|
let hasWitness = false;
|
||||||
@ -415,7 +462,7 @@ export class Common {
|
|||||||
return flags;
|
return flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getTransactionFlags(tx: TransactionExtended): number {
|
static getTransactionFlags(tx: TransactionExtended, height?: number): number {
|
||||||
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||||
|
|
||||||
// Update variable flags (CPFP, RBF)
|
// Update variable flags (CPFP, RBF)
|
||||||
@ -548,7 +595,7 @@ export class Common {
|
|||||||
if (hasFakePubkey) {
|
if (hasFakePubkey) {
|
||||||
flags |= TransactionFlags.fake_pubkey;
|
flags |= TransactionFlags.fake_pubkey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fast but bad heuristic to detect possible coinjoins
|
// fast but bad heuristic to detect possible coinjoins
|
||||||
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
|
// (at least 5 inputs and 5 outputs, less than half of which are unique amounts, with no address reuse)
|
||||||
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
|
const addressReuse = Object.keys(reusedOutputAddresses).reduce((acc, key) => Math.max(acc, (reusedInputAddresses[key] || 0) + (reusedOutputAddresses[key] || 0)), 0) > 1;
|
||||||
@ -564,17 +611,17 @@ export class Common {
|
|||||||
flags |= TransactionFlags.batch_payout;
|
flags |= TransactionFlags.batch_payout;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isNonStandard(tx)) {
|
if (this.isNonStandard(tx, height)) {
|
||||||
flags |= TransactionFlags.nonstandard;
|
flags |= TransactionFlags.nonstandard;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Number(flags);
|
return Number(flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
static classifyTransaction(tx: TransactionExtended): TransactionClassified {
|
static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
|
||||||
let flags = 0;
|
let flags = 0;
|
||||||
try {
|
try {
|
||||||
flags = Common.getTransactionFlags(tx);
|
flags = Common.getTransactionFlags(tx, height);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
|
logger.warn('Failed to add classification flags to transaction: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
@ -585,8 +632,8 @@ export class Common {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static classifyTransactions(txs: TransactionExtended[]): TransactionClassified[] {
|
static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
|
||||||
return txs.map(Common.classifyTransaction);
|
return txs.map(tx => Common.classifyTransaction(tx, height));
|
||||||
}
|
}
|
||||||
|
|
||||||
static stripTransaction(tx: TransactionExtended): TransactionStripped {
|
static stripTransaction(tx: TransactionExtended): TransactionStripped {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { Acceleration } from './acceleration/acceleration';
|
|||||||
const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
|
const CPFP_UPDATE_INTERVAL = 60_000; // update CPFP info at most once per 60s per transaction
|
||||||
const MAX_CLUSTER_ITERATIONS = 100;
|
const MAX_CLUSTER_ITERATIONS = 100;
|
||||||
|
|
||||||
export function calculateFastBlockCpfp(height: number, transactions: TransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
|
export function calculateFastBlockCpfp(height: number, transactions: MempoolTransactionExtended[], saveRelatives: boolean = false): CpfpSummary {
|
||||||
const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
|
const clusters: CpfpCluster[] = []; // list of all cpfp clusters in this block
|
||||||
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
|
const clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
|
||||||
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
|
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
|
||||||
@ -93,6 +93,7 @@ export function calculateFastBlockCpfp(height: number, transactions: Transaction
|
|||||||
return {
|
return {
|
||||||
transactions,
|
transactions,
|
||||||
clusters,
|
clusters,
|
||||||
|
version: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,6 +160,7 @@ export function calculateGoodBlockCpfp(height: number, transactions: MempoolTran
|
|||||||
return {
|
return {
|
||||||
transactions: transactions.map(tx => txMap[tx.txid]),
|
transactions: transactions.map(tx => txMap[tx.txid]),
|
||||||
clusters: clusterArray,
|
clusters: clusterArray,
|
||||||
|
version: 2,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
|||||||
import { RowDataPacket } from 'mysql2';
|
import { RowDataPacket } from 'mysql2';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 80;
|
private static currentVersion = 93;
|
||||||
private queryTimeout = 3600_000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -653,9 +653,11 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"');
|
await this.$executeQuery('ALTER TABLE `prices` ADD `TRY` float DEFAULT "-1"');
|
||||||
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
|
await this.$executeQuery('ALTER TABLE `prices` ADD `ZAR` float DEFAULT "-1"');
|
||||||
|
|
||||||
await this.$executeQuery('TRUNCATE hashrates');
|
if (isBitcoin === true) {
|
||||||
await this.$executeQuery('TRUNCATE difficulty_adjustments');
|
await this.$executeQuery('TRUNCATE hashrates');
|
||||||
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
|
await this.$executeQuery('TRUNCATE difficulty_adjustments');
|
||||||
|
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
|
||||||
|
}
|
||||||
|
|
||||||
await this.updateToSchemaVersion(75);
|
await this.updateToSchemaVersion(75);
|
||||||
}
|
}
|
||||||
@ -691,6 +693,114 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL');
|
await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL');
|
||||||
await this.updateToSchemaVersion(80);
|
await this.updateToSchemaVersion(80);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 81 && isBitcoin === true) {
|
||||||
|
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 "[]"');
|
||||||
|
await this.updateToSchemaVersion(81);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 82 && isBitcoin === true && config.MEMPOOL.NETWORK === 'mainnet') {
|
||||||
|
await this.$fixBadV1AuditBlocks();
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1305,6 +1415,28 @@ class DatabaseMigration {
|
|||||||
logger.warn(`Failed to migrate cpfp transaction data`);
|
logger.warn(`Failed to migrate cpfp transaction data`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async $fixBadV1AuditBlocks(): Promise<void> {
|
||||||
|
const badBlocks = [
|
||||||
|
'000000000000000000011ad49227fc8c9ba0ca96ad2ebce41a862f9a244478dc',
|
||||||
|
'000000000000000000010ac1f68b3080153f2826ffddc87ceffdd68ed97d6960',
|
||||||
|
'000000000000000000024cbdafeb2660ae8bd2947d166e7fe15d1689e86b2cf7',
|
||||||
|
'00000000000000000002e1dbfbf6ae057f331992a058b822644b368034f87286',
|
||||||
|
'0000000000000000000019973b2778f08ad6d21e083302ff0833d17066921ebb',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const hash of badBlocks) {
|
||||||
|
try {
|
||||||
|
await this.$executeQuery(`
|
||||||
|
UPDATE blocks_audits
|
||||||
|
SET prioritized_txs = '[]'
|
||||||
|
WHERE hash = '${hash}'
|
||||||
|
`, true);
|
||||||
|
} catch (e) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new DatabaseMigration();
|
export default new DatabaseMigration();
|
||||||
|
|||||||
@ -257,6 +257,7 @@ class DiskCache {
|
|||||||
trees: rbfData.rbf.trees,
|
trees: rbfData.rbf.trees,
|
||||||
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
|
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
|
||||||
mempool: memPool.getMempool(),
|
mempool: memPool.getMempool(),
|
||||||
|
spendMap: memPool.getSpendMap(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { Application, Request, Response } from 'express';
|
import { Application, Request, Response } from 'express';
|
||||||
import channelsApi from './channels.api';
|
import channelsApi from './channels.api';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class ChannelsRoutes {
|
class ChannelsRoutes {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -22,7 +23,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +39,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,11 +54,11 @@ class ChannelsRoutes {
|
|||||||
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
|
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
|
||||||
|
|
||||||
if (index < -1) {
|
if (index < -1) {
|
||||||
res.status(400).send('Invalid index');
|
handleError(req, res, 400, 'Invalid index');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (['open', 'active', 'closed'].includes(status) === false) {
|
if (['open', 'active', 'closed'].includes(status) === false) {
|
||||||
res.status(400).send('Invalid status');
|
handleError(req, res, 400, 'Invalid status');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,14 +70,14 @@ 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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
|
private async $getChannelsByTransactionIds(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (!Array.isArray(req.query.txId)) {
|
if (!Array.isArray(req.query.txId)) {
|
||||||
res.status(400).send('Not an array');
|
handleError(req, res, 400, 'Not an array');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const txIds: string[] = [];
|
const txIds: string[] = [];
|
||||||
@ -107,7 +108,7 @@ class ChannelsRoutes {
|
|||||||
|
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +120,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +133,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,8 @@ import { Application, Request, Response } from 'express';
|
|||||||
import nodesApi from './nodes.api';
|
import nodesApi from './nodes.api';
|
||||||
import channelsApi from './channels.api';
|
import channelsApi from './channels.api';
|
||||||
import statisticsApi from './statistics.api';
|
import statisticsApi from './statistics.api';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class GeneralLightningRoutes {
|
class GeneralLightningRoutes {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
@ -27,7 +29,7 @@ class GeneralLightningRoutes {
|
|||||||
channels: channels,
|
channels: channels,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
|
|||||||
import nodesApi from './nodes.api';
|
import nodesApi from './nodes.api';
|
||||||
import DB from '../../database';
|
import DB from '../../database';
|
||||||
import { INodesRanking } from '../../mempool.interfaces';
|
import { INodesRanking } from '../../mempool.interfaces';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class NodesRoutes {
|
class NodesRoutes {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -31,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,13 +182,13 @@ class NodesRoutes {
|
|||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,7 +196,7 @@ class NodesRoutes {
|
|||||||
try {
|
try {
|
||||||
const node = await nodesApi.$getNode(req.params.public_key);
|
const node = await nodesApi.$getNode(req.params.public_key);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
res.status(404).send('Node not found');
|
handleError(req, res, 404, 'Node not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
@ -203,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,7 +224,7 @@ class NodesRoutes {
|
|||||||
try {
|
try {
|
||||||
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
|
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
res.status(404).send('Node not found');
|
handleError(req, res, 404, 'Node not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.header('Pragma', 'public');
|
res.header('Pragma', 'public');
|
||||||
@ -231,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,7 +248,7 @@ class NodesRoutes {
|
|||||||
topByChannels: topChannelsNodes,
|
topByChannels: topChannelsNodes,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -271,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -307,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,7 +323,7 @@ class NodesRoutes {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (country.length === 0) {
|
if (country.length === 0) {
|
||||||
res.status(404).send(`This country does not exist or does not host any lightning nodes on clearnet`);
|
handleError(req, res, 404, `This country does not exist or does not host any lightning nodes on clearnet`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,7 +336,7 @@ class NodesRoutes {
|
|||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,7 +350,7 @@ class NodesRoutes {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isp.length === 0) {
|
if (isp.length === 0) {
|
||||||
res.status(404).send(`This ISP does not exist or does not host any lightning nodes on clearnet`);
|
handleError(req, res, 404, `This ISP does not exist or does not host any lightning nodes on clearnet`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,7 +363,7 @@ class NodesRoutes {
|
|||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Application, Request, Response } from 'express';
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import elementsParser from './elements-parser';
|
import elementsParser from './elements-parser';
|
||||||
import icons from './icons';
|
import icons from './icons';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class LiquidRoutes {
|
class LiquidRoutes {
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application) {
|
||||||
@ -42,7 +43,7 @@ class LiquidRoutes {
|
|||||||
res.setHeader('content-length', result.length);
|
res.setHeader('content-length', result.length);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} else {
|
} else {
|
||||||
res.status(404).send('Asset icon not found');
|
handleError(req, res, 404, 'Asset icon not found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,7 +52,7 @@ class LiquidRoutes {
|
|||||||
if (result) {
|
if (result) {
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} else {
|
} else {
|
||||||
res.status(404).send('Asset icons not found');
|
handleError(req, res, 404, 'Asset icons not found');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -369,7 +369,7 @@ class MempoolBlocks {
|
|||||||
const lastBlockIndex = blocks.length - 1;
|
const lastBlockIndex = blocks.length - 1;
|
||||||
let hasBlockStack = blocks.length >= 8;
|
let hasBlockStack = blocks.length >= 8;
|
||||||
let stackWeight;
|
let stackWeight;
|
||||||
let feeStatsCalculator: OnlineFeeStatsCalculator | void;
|
let feeStatsCalculator: OnlineFeeStatsCalculator | null = null;
|
||||||
if (hasBlockStack) {
|
if (hasBlockStack) {
|
||||||
if (blockWeights && blockWeights[7] !== null) {
|
if (blockWeights && blockWeights[7] !== null) {
|
||||||
stackWeight = blockWeights[7];
|
stackWeight = blockWeights[7];
|
||||||
@ -380,28 +380,36 @@ class MempoolBlocks {
|
|||||||
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
|
feeStatsCalculator = new OnlineFeeStatsCalculator(stackWeight, 0.5, [10, 20, 30, 40, 50, 60, 70, 80, 90]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ancestors: Ancestor[] = [];
|
||||||
|
const descendants: Ancestor[] = [];
|
||||||
|
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];
|
||||||
if (mempoolTx) {
|
if (mempoolTx) {
|
||||||
const ancestors: Ancestor[] = [];
|
// ugly micro-optimization to avoid allocating new arrays
|
||||||
const descendants: Ancestor[] = [];
|
ancestors.length = 0;
|
||||||
|
descendants.length = 0;
|
||||||
let matched = false;
|
let matched = false;
|
||||||
cluster.forEach(txid => {
|
cluster.forEach(txid => {
|
||||||
|
ancestor = mempool[txid];
|
||||||
if (txid === memberTxid) {
|
if (txid === memberTxid) {
|
||||||
matched = true;
|
matched = true;
|
||||||
} else {
|
} else {
|
||||||
if (!mempool[txid]) {
|
if (!ancestor) {
|
||||||
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
|
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const relative = {
|
const relative = {
|
||||||
txid: txid,
|
txid: txid,
|
||||||
fee: mempool[txid].fee,
|
fee: ancestor.fee,
|
||||||
weight: (mempool[txid].adjustedVsize * 4),
|
weight: (ancestor.adjustedVsize * 4),
|
||||||
};
|
};
|
||||||
if (matched) {
|
if (matched) {
|
||||||
descendants.push(relative);
|
descendants.push(relative);
|
||||||
mempoolTx.lastBoosted = Math.max(mempoolTx.lastBoosted || 0, mempool[txid].firstSeen || 0);
|
if (!mempoolTx.lastBoosted || (ancestor.firstSeen && ancestor.firstSeen > mempoolTx.lastBoosted)) {
|
||||||
|
mempoolTx.lastBoosted = ancestor.firstSeen;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ancestors.push(relative);
|
ancestors.push(relative);
|
||||||
}
|
}
|
||||||
@ -410,7 +418,20 @@ class MempoolBlocks {
|
|||||||
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
|
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
|
||||||
mempoolTx.cpfpDirty = true;
|
mempoolTx.cpfpDirty = true;
|
||||||
}
|
}
|
||||||
Object.assign(mempoolTx, {ancestors, descendants, bestDescendant: null, cpfpChecked: true});
|
// ugly micro-optimization to avoid allocating new arrays or objects
|
||||||
|
if (mempoolTx.ancestors) {
|
||||||
|
mempoolTx.ancestors.length = 0;
|
||||||
|
} else {
|
||||||
|
mempoolTx.ancestors = [];
|
||||||
|
}
|
||||||
|
if (mempoolTx.descendants) {
|
||||||
|
mempoolTx.descendants.length = 0;
|
||||||
|
} else {
|
||||||
|
mempoolTx.descendants = [];
|
||||||
|
}
|
||||||
|
mempoolTx.ancestors.push(...ancestors);
|
||||||
|
mempoolTx.descendants.push(...descendants);
|
||||||
|
mempoolTx.cpfpChecked = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -420,7 +441,10 @@ class MempoolBlocks {
|
|||||||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||||
// update this thread's mempool with the results
|
// update this thread's mempool with the results
|
||||||
let mempoolTx: MempoolTransactionExtended;
|
let mempoolTx: MempoolTransactionExtended;
|
||||||
const mempoolBlocks: MempoolBlockWithTransactions[] = blocks.map((block, blockIndex) => {
|
let acceleration: Acceleration;
|
||||||
|
const mempoolBlocks: MempoolBlockWithTransactions[] = [];
|
||||||
|
for (let blockIndex = 0; blockIndex < blocks.length; blockIndex++) {
|
||||||
|
const block = blocks[blockIndex];
|
||||||
let totalSize = 0;
|
let totalSize = 0;
|
||||||
let totalVsize = 0;
|
let totalVsize = 0;
|
||||||
let totalWeight = 0;
|
let totalWeight = 0;
|
||||||
@ -436,8 +460,9 @@ class MempoolBlocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const txid of block) {
|
for (let i = 0; i < block.length; i++) {
|
||||||
if (txid) {
|
const txid = block[i];
|
||||||
|
if (txid in mempool) {
|
||||||
mempoolTx = mempool[txid];
|
mempoolTx = mempool[txid];
|
||||||
// save position in projected blocks
|
// save position in projected blocks
|
||||||
mempoolTx.position = {
|
mempoolTx.position = {
|
||||||
@ -445,28 +470,40 @@ class MempoolBlocks {
|
|||||||
vsize: totalVsize + (mempoolTx.vsize / 2),
|
vsize: totalVsize + (mempoolTx.vsize / 2),
|
||||||
};
|
};
|
||||||
|
|
||||||
const acceleration = accelerations[txid];
|
if (txid in accelerations) {
|
||||||
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
acceleration = accelerations[txid];
|
||||||
if (!mempoolTx.acceleration) {
|
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
||||||
mempoolTx.cpfpDirty = true;
|
if (!mempoolTx.acceleration) {
|
||||||
}
|
mempoolTx.cpfpDirty = true;
|
||||||
mempoolTx.acceleration = true;
|
}
|
||||||
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
|
mempoolTx.acceleration = true;
|
||||||
mempoolTx.acceleratedAt = acceleration?.added;
|
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
|
||||||
for (const ancestor of mempoolTx.ancestors || []) {
|
mempoolTx.acceleratedAt = acceleration?.added;
|
||||||
if (!mempool[ancestor.txid].acceleration) {
|
mempoolTx.feeDelta = acceleration?.feeDelta;
|
||||||
mempool[ancestor.txid].cpfpDirty = true;
|
for (const ancestor of mempoolTx.ancestors || []) {
|
||||||
|
if (!(ancestor.txid in mempool)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!mempool[ancestor.txid].acceleration) {
|
||||||
|
mempool[ancestor.txid].cpfpDirty = true;
|
||||||
|
}
|
||||||
|
mempool[ancestor.txid].acceleration = true;
|
||||||
|
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
|
||||||
|
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
|
||||||
|
mempool[ancestor.txid].feeDelta = mempoolTx.feeDelta;
|
||||||
|
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mempoolTx.acceleration) {
|
||||||
|
mempoolTx.cpfpDirty = true;
|
||||||
|
delete mempoolTx.acceleration;
|
||||||
}
|
}
|
||||||
mempool[ancestor.txid].acceleration = true;
|
|
||||||
mempool[ancestor.txid].acceleratedBy = mempoolTx.acceleratedBy;
|
|
||||||
mempool[ancestor.txid].acceleratedAt = mempoolTx.acceleratedAt;
|
|
||||||
isAcceleratedBy[ancestor.txid] = mempoolTx.acceleratedBy;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (mempoolTx.acceleration) {
|
if (mempoolTx.acceleration) {
|
||||||
mempoolTx.cpfpDirty = true;
|
mempoolTx.cpfpDirty = true;
|
||||||
|
delete mempoolTx.acceleration;
|
||||||
}
|
}
|
||||||
delete mempoolTx.acceleration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// online calculation of stack-of-blocks fee stats
|
// online calculation of stack-of-blocks fee stats
|
||||||
@ -484,7 +521,7 @@ class MempoolBlocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.dataToMempoolBlocks(
|
mempoolBlocks[blockIndex] = this.dataToMempoolBlocks(
|
||||||
block,
|
block,
|
||||||
transactions,
|
transactions,
|
||||||
totalSize,
|
totalSize,
|
||||||
@ -492,7 +529,7 @@ class MempoolBlocks {
|
|||||||
totalFees,
|
totalFees,
|
||||||
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
|
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
if (saveResults) {
|
if (saveResults) {
|
||||||
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
|
const deltas = this.calculateMempoolDeltas(this.mempoolBlocks, mempoolBlocks);
|
||||||
@ -654,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);
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
@ -19,12 +20,13 @@ class Mempool {
|
|||||||
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
||||||
private mempoolCandidates: { [txid: string ]: boolean } = {};
|
private mempoolCandidates: { [txid: string ]: boolean } = {};
|
||||||
private spendMap = new Map<string, MempoolTransactionExtended>();
|
private spendMap = new Map<string, MempoolTransactionExtended>();
|
||||||
|
private recentlyDeleted: MempoolTransactionExtended[][] = []; // buffer of transactions deleted in recent mempool updates
|
||||||
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
||||||
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
|
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
|
||||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
|
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void) | undefined;
|
||||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
|
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
|
||||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
|
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
|
||||||
|
|
||||||
private accelerations: { [txId: string]: Acceleration } = {};
|
private accelerations: { [txId: string]: Acceleration } = {};
|
||||||
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
|
private accelerationPositions: { [txid: string]: { poolId: number, pool: string, block: number, vsize: number }[] } = {};
|
||||||
@ -74,12 +76,12 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
|
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void {
|
||||||
this.mempoolChangedCallback = fn;
|
this.mempoolChangedCallback = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
|
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
|
||||||
candidates?: GbtCandidates) => Promise<void>): void {
|
candidates?: GbtCandidates) => Promise<void>): void {
|
||||||
this.$asyncMempoolChangedCallback = fn;
|
this.$asyncMempoolChangedCallback = fn;
|
||||||
}
|
}
|
||||||
@ -206,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
|
||||||
@ -353,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;
|
||||||
}
|
}
|
||||||
@ -362,12 +364,15 @@ class Mempool {
|
|||||||
|
|
||||||
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
|
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
|
||||||
|
|
||||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
this.recentlyDeleted.unshift(deletedTransactions);
|
||||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
|
this.recentlyDeleted.length = Math.min(this.recentlyDeleted.length, 10); // truncate to the last 10 mempool updates
|
||||||
|
|
||||||
|
if (this.mempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length)) {
|
||||||
|
this.mempoolChangedCallback(this.mempoolCache, newTransactions, this.recentlyDeleted, accelerationDelta);
|
||||||
}
|
}
|
||||||
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length || candidatesChanged)) {
|
if (this.$asyncMempoolChangedCallback && (hasChange || newTransactions.length || deletedTransactions.length || candidatesChanged)) {
|
||||||
this.updateTimerProgress(timer, 'running async mempool callback');
|
this.updateTimerProgress(timer, 'running async mempool callback');
|
||||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta, candidates);
|
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, this.recentlyDeleted, accelerationDelta, candidates);
|
||||||
this.updateTimerProgress(timer, 'completed async mempool callback');
|
this.updateTimerProgress(timer, 'completed async mempool callback');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,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 [];
|
||||||
@ -541,16 +499,7 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): void {
|
public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
|
||||||
for (const rbfTransaction in rbfTransactions) {
|
|
||||||
if (this.mempoolCache[rbfTransaction] && rbfTransactions[rbfTransaction]?.length) {
|
|
||||||
// Store replaced transactions
|
|
||||||
rbfCache.add(rbfTransactions[rbfTransaction], this.mempoolCache[rbfTransaction]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleMinedRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
|
|
||||||
for (const rbfTransaction in rbfTransactions) {
|
for (const rbfTransaction in rbfTransactions) {
|
||||||
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
|
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
|
||||||
// Store replaced transactions
|
// Store replaced transactions
|
||||||
|
|||||||
@ -337,7 +337,7 @@ export function makeBlockTemplate(candidates: MempoolTransactionExtended[], acce
|
|||||||
let failures = 0;
|
let failures = 0;
|
||||||
while (mempoolArray.length || modified.length) {
|
while (mempoolArray.length || modified.length) {
|
||||||
// skip invalid transactions
|
// skip invalid transactions
|
||||||
while (mempoolArray[0].used || mempoolArray[0].modified) {
|
while (mempoolArray[0]?.used || mempoolArray[0]?.modified) {
|
||||||
mempoolArray.shift();
|
mempoolArray.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import mining from "./mining";
|
|||||||
import PricesRepository from '../../repositories/PricesRepository';
|
import PricesRepository from '../../repositories/PricesRepository';
|
||||||
import AccelerationRepository from '../../repositories/AccelerationRepository';
|
import AccelerationRepository from '../../repositories/AccelerationRepository';
|
||||||
import accelerationApi from '../services/acceleration';
|
import accelerationApi from '../services/acceleration';
|
||||||
|
import { handleError } from '../../utils/api';
|
||||||
|
|
||||||
class MiningRoutes {
|
class MiningRoutes {
|
||||||
public initRoutes(app: Application) {
|
public initRoutes(app: Application) {
|
||||||
@ -53,12 +54,12 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||||
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
|
if (['testnet', 'signet', 'liquidtestnet'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Prices are not available on testnets.');
|
handleError(req, res, 400, 'Prices are not available on testnets.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
|
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
|
||||||
const currency = req.query.currency as string;
|
const currency = req.query.currency as string;
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
if (timestamp && currency) {
|
if (timestamp && currency) {
|
||||||
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
|
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
|
||||||
@ -71,7 +72,7 @@ class MiningRoutes {
|
|||||||
}
|
}
|
||||||
res.status(200).send(response);
|
res.status(200).send(response);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,9 +85,9 @@ class MiningRoutes {
|
|||||||
res.json(stats);
|
res.json(stats);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
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) {
|
||||||
res.status(404).send(e.message);
|
handleError(req, res, 404, e.message);
|
||||||
} else {
|
} else {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,9 +104,9 @@ class MiningRoutes {
|
|||||||
res.json(poolBlocks);
|
res.json(poolBlocks);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
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) {
|
||||||
res.status(404).send(e.message);
|
handleError(req, res, 404, e.message);
|
||||||
} else {
|
} else {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -129,7 +130,7 @@ class MiningRoutes {
|
|||||||
res.json(pools);
|
res.json(pools);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -143,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,9 +173,9 @@ class MiningRoutes {
|
|||||||
res.json(hashrates);
|
res.json(hashrates);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
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) {
|
||||||
res.status(404).send(e.message);
|
handleError(req, res, 404, e.message);
|
||||||
} else {
|
} else {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -182,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');
|
||||||
@ -203,7 +204,7 @@ class MiningRoutes {
|
|||||||
currentDifficulty: currentDifficulty,
|
currentDifficulty: currentDifficulty,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,7 +282,7 @@ class MiningRoutes {
|
|||||||
weights: blockWeights
|
weights: blockWeights
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -317,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,7 +327,7 @@ class MiningRoutes {
|
|||||||
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
|
const audit = await BlocksAuditsRepository.$getBlockAudit(req.params.hash);
|
||||||
|
|
||||||
if (!audit) {
|
if (!audit) {
|
||||||
res.status(204).send(`This block has not been audited.`);
|
handleError(req, res, 204, `This block has not been audited.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,12 +395,12 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Acceleration data is not available.');
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
|
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -409,13 +410,13 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Acceleration data is not available.');
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,12 +426,12 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Acceleration data is not available.');
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -440,12 +441,12 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('Acceleration data is not available.');
|
handleError(req, res, 400, 'Acceleration data is not available.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -455,28 +456,24 @@ class MiningRoutes {
|
|||||||
res.header('Cache-control', 'public');
|
res.header('Cache-control', 'public');
|
||||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS || ['testnet', 'signet', 'liquidtestnet', 'liquid'].includes(config.MEMPOOL.NETWORK)) {
|
||||||
res.status(400).send('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) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $requestAcceleration(req: Request, res: Response): Promise<void> {
|
private async $requestAcceleration(req: Request, res: Response): Promise<void> {
|
||||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS || config.MEMPOOL.OFFICIAL) {
|
|
||||||
res.status(405).send('not available.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
res.setHeader('Pragma', 'no-cache');
|
res.setHeader('Pragma', 'no-cache');
|
||||||
res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
|
res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
|
||||||
res.setHeader('expires', -1);
|
res.setHeader('expires', -1);
|
||||||
try {
|
try {
|
||||||
accelerationApi.accelerationRequested(req.params.txid);
|
accelerationApi.accelerationRequested(req.params.txid);
|
||||||
res.status(200).send('ok');
|
res.status(200).send();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
handleError(req, res, 500, e instanceof Error ? e.message : e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -44,6 +44,22 @@ interface CacheEvent {
|
|||||||
value?: any,
|
value?: any,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton for tracking RBF trees
|
||||||
|
*
|
||||||
|
* Maintains a set of RBF trees, where each tree represents a sequence of
|
||||||
|
* consecutive RBF replacements.
|
||||||
|
*
|
||||||
|
* Trees are identified by the txid of the root transaction.
|
||||||
|
*
|
||||||
|
* To maintain consistency, the following invariants must be upheld:
|
||||||
|
* - Symmetry: replacedBy(A) = B <=> A in replaces(B)
|
||||||
|
* - Unique id: treeMap(treeMap(X)) = treeMap(X)
|
||||||
|
* - Unique tree: A in replaces(B) => treeMap(A) == treeMap(B)
|
||||||
|
* - Existence: X in treeMap => treeMap(X) in rbfTrees
|
||||||
|
* - Completeness: X in replacedBy => X in treeMap, Y in replaces => Y in treeMap
|
||||||
|
*/
|
||||||
|
|
||||||
class RbfCache {
|
class RbfCache {
|
||||||
private replacedBy: Map<string, string> = new Map();
|
private replacedBy: Map<string, string> = new Map();
|
||||||
private replaces: Map<string, string[]> = new Map();
|
private replaces: Map<string, string[]> = new Map();
|
||||||
@ -61,6 +77,10 @@ class RbfCache {
|
|||||||
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
setInterval(this.cleanup.bind(this), 1000 * 60 * 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Low level cache operations
|
||||||
|
*/
|
||||||
|
|
||||||
private addTx(txid: string, tx: MempoolTransactionExtended): void {
|
private addTx(txid: string, tx: MempoolTransactionExtended): void {
|
||||||
this.txs.set(txid, tx);
|
this.txs.set(txid, tx);
|
||||||
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
|
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
|
||||||
@ -92,6 +112,12 @@ class RbfCache {
|
|||||||
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
|
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic data structure operations
|
||||||
|
* must uphold tree invariants
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
||||||
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
||||||
return;
|
return;
|
||||||
@ -114,6 +140,10 @@ class RbfCache {
|
|||||||
if (!replacedTx.rbf) {
|
if (!replacedTx.rbf) {
|
||||||
txFullRbf = true;
|
txFullRbf = true;
|
||||||
}
|
}
|
||||||
|
if (this.replacedBy.has(replacedTx.txid)) {
|
||||||
|
// should never happen
|
||||||
|
continue;
|
||||||
|
}
|
||||||
this.replacedBy.set(replacedTx.txid, newTx.txid);
|
this.replacedBy.set(replacedTx.txid, newTx.txid);
|
||||||
if (this.treeMap.has(replacedTx.txid)) {
|
if (this.treeMap.has(replacedTx.txid)) {
|
||||||
const treeId = this.treeMap.get(replacedTx.txid);
|
const treeId = this.treeMap.get(replacedTx.txid);
|
||||||
@ -140,18 +170,47 @@ class RbfCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
newTx.fullRbf = txFullRbf;
|
newTx.fullRbf = txFullRbf;
|
||||||
const treeId = replacedTrees[0].tx.txid;
|
|
||||||
const newTree = {
|
const newTree = {
|
||||||
tx: newTx,
|
tx: newTx,
|
||||||
time: newTime,
|
time: newTime,
|
||||||
fullRbf: treeFullRbf,
|
fullRbf: treeFullRbf,
|
||||||
replaces: replacedTrees
|
replaces: replacedTrees
|
||||||
};
|
};
|
||||||
this.addTree(treeId, newTree);
|
this.addTree(newTree.tx.txid, newTree);
|
||||||
this.updateTreeMap(treeId, newTree);
|
this.updateTreeMap(newTree.tx.txid, newTree);
|
||||||
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
this.replaces.set(newTx.txid, replacedTrees.map(tree => tree.tx.txid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public mined(txid): void {
|
||||||
|
if (!this.txs.has(txid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const treeId = this.treeMap.get(txid);
|
||||||
|
if (treeId && this.rbfTrees.has(treeId)) {
|
||||||
|
const tree = this.rbfTrees.get(treeId);
|
||||||
|
if (tree) {
|
||||||
|
this.setTreeMined(tree, txid);
|
||||||
|
tree.mined = true;
|
||||||
|
this.dirtyTrees.add(treeId);
|
||||||
|
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.evict(txid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// flag a transaction as removed from the mempool
|
||||||
|
public evict(txid: string, fast: boolean = false): void {
|
||||||
|
this.evictionCount++;
|
||||||
|
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
||||||
|
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
||||||
|
this.addExpiration(txid, expiryTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only public interface
|
||||||
|
*/
|
||||||
|
|
||||||
public has(txId: string): boolean {
|
public has(txId: string): boolean {
|
||||||
return this.txs.has(txId);
|
return this.txs.has(txId);
|
||||||
}
|
}
|
||||||
@ -232,32 +291,6 @@ class RbfCache {
|
|||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public mined(txid): void {
|
|
||||||
if (!this.txs.has(txid)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const treeId = this.treeMap.get(txid);
|
|
||||||
if (treeId && this.rbfTrees.has(treeId)) {
|
|
||||||
const tree = this.rbfTrees.get(treeId);
|
|
||||||
if (tree) {
|
|
||||||
this.setTreeMined(tree, txid);
|
|
||||||
tree.mined = true;
|
|
||||||
this.dirtyTrees.add(treeId);
|
|
||||||
this.cacheQueue.push({ op: CacheOp.Change, type: 'tree', txid: treeId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.evict(txid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// flag a transaction as removed from the mempool
|
|
||||||
public evict(txid: string, fast: boolean = false): void {
|
|
||||||
this.evictionCount++;
|
|
||||||
if (this.txs.has(txid) && (fast || !this.expiring.has(txid))) {
|
|
||||||
const expiryTime = fast ? Date.now() + (1000 * 60 * 10) : Date.now() + (1000 * 86400); // 24 hours
|
|
||||||
this.addExpiration(txid, expiryTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// is the transaction involved in a full rbf replacement?
|
// is the transaction involved in a full rbf replacement?
|
||||||
public isFullRbf(txid: string): boolean {
|
public isFullRbf(txid: string): boolean {
|
||||||
const treeId = this.treeMap.get(txid);
|
const treeId = this.treeMap.get(txid);
|
||||||
@ -271,6 +304,10 @@ class RbfCache {
|
|||||||
return tree?.fullRbf;
|
return tree?.fullRbf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache maintenance & utility functions
|
||||||
|
*/
|
||||||
|
|
||||||
private cleanup(): void {
|
private cleanup(): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const txid of this.expiring.keys()) {
|
for (const txid of this.expiring.keys()) {
|
||||||
@ -299,10 +336,6 @@ class RbfCache {
|
|||||||
for (const tx of (replaces || [])) {
|
for (const tx of (replaces || [])) {
|
||||||
// recursively remove prior versions from the cache
|
// recursively remove prior versions from the cache
|
||||||
this.replacedBy.delete(tx);
|
this.replacedBy.delete(tx);
|
||||||
// if this is the id of a tree, remove that too
|
|
||||||
if (this.treeMap.get(tx) === tx) {
|
|
||||||
this.removeTree(tx);
|
|
||||||
}
|
|
||||||
this.remove(tx);
|
this.remove(tx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -370,14 +403,21 @@ class RbfCache {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async load({ txs, trees, expiring, mempool }): Promise<void> {
|
public async load({ txs, trees, expiring, mempool, spendMap }): Promise<void> {
|
||||||
try {
|
try {
|
||||||
txs.forEach(txEntry => {
|
txs.forEach(txEntry => {
|
||||||
this.txs.set(txEntry.value.txid, txEntry.value);
|
this.txs.set(txEntry.value.txid, txEntry.value);
|
||||||
});
|
});
|
||||||
this.staleCount = 0;
|
this.staleCount = 0;
|
||||||
for (const deflatedTree of trees) {
|
for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) {
|
||||||
await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
const tree = await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
||||||
|
if (tree) {
|
||||||
|
this.addTree(tree.tx.txid, tree);
|
||||||
|
this.updateTreeMap(tree.tx.txid, tree);
|
||||||
|
if (tree.mined) {
|
||||||
|
this.evict(tree.tx.txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
expiring.forEach(expiringEntry => {
|
expiring.forEach(expiringEntry => {
|
||||||
if (this.txs.has(expiringEntry.key)) {
|
if (this.txs.has(expiringEntry.key)) {
|
||||||
@ -385,6 +425,31 @@ class RbfCache {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.staleCount = 0;
|
this.staleCount = 0;
|
||||||
|
|
||||||
|
// connect cached trees to current mempool transactions
|
||||||
|
const conflicts: Record<string, { replacedBy: MempoolTransactionExtended, replaces: Set<MempoolTransactionExtended> }> = {};
|
||||||
|
for (const tree of this.rbfTrees.values()) {
|
||||||
|
const tx = this.getTx(tree.tx.txid);
|
||||||
|
if (!tx || tree.mined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const vin of tx.vin) {
|
||||||
|
const conflict = spendMap.get(`${vin.txid}:${vin.vout}`);
|
||||||
|
if (conflict && conflict.txid !== tx.txid) {
|
||||||
|
if (!conflicts[conflict.txid]) {
|
||||||
|
conflicts[conflict.txid] = {
|
||||||
|
replacedBy: conflict,
|
||||||
|
replaces: new Set(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
conflicts[conflict.txid].replaces.add(tx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const { replacedBy, replaces } of Object.values(conflicts)) {
|
||||||
|
this.add([...replaces.values()], replacedBy);
|
||||||
|
}
|
||||||
|
|
||||||
await this.checkTrees();
|
await this.checkTrees();
|
||||||
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
|
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
@ -426,6 +491,12 @@ class RbfCache {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if this tx is already in the cache, return early
|
||||||
|
if (this.treeMap.has(txid)) {
|
||||||
|
this.removeTree(deflated.key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// recursively reconstruct child trees
|
// recursively reconstruct child trees
|
||||||
for (const childId of treeInfo.replaces) {
|
for (const childId of treeInfo.replaces) {
|
||||||
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
|
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
|
||||||
@ -457,10 +528,6 @@ class RbfCache {
|
|||||||
fullRbf: treeInfo.fullRbf,
|
fullRbf: treeInfo.fullRbf,
|
||||||
replaces,
|
replaces,
|
||||||
};
|
};
|
||||||
this.treeMap.set(txid, root);
|
|
||||||
if (root === txid) {
|
|
||||||
this.addTree(root, tree);
|
|
||||||
}
|
|
||||||
return tree;
|
return tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -511,6 +578,7 @@ class RbfCache {
|
|||||||
processTxs(txs);
|
processTxs(txs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// evict missing transactions
|
||||||
for (const txid of txids) {
|
for (const txid of txids) {
|
||||||
if (!found[txid]) {
|
if (!found[txid]) {
|
||||||
this.evict(txid, false);
|
this.evict(txid, false);
|
||||||
|
|||||||
@ -365,6 +365,7 @@ class RedisCache {
|
|||||||
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
|
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
|
||||||
expiring: rbfExpirations,
|
expiring: rbfExpirations,
|
||||||
mempool: memPool.getMempool(),
|
mempool: memPool.getMempool(),
|
||||||
|
spendMap: memPool.getSpendMap(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,13 +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 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +65,9 @@ class AccelerationApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public accelerationRequested(txid: string): void {
|
public accelerationRequested(txid: string): void {
|
||||||
this.myAccelerations[txid] = { status: 'requested', added: Date.now() };
|
if (this.onDemandPollingEnabled) {
|
||||||
|
this.myAccelerations[txid] = { status: 'requested', added: Date.now() };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public accelerationConfirmed(): void {
|
public accelerationConfirmed(): void {
|
||||||
@ -69,11 +84,18 @@ class AccelerationApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $updateAccelerations(): Promise<Acceleration[] | null> {
|
public async $updateAccelerations(): Promise<Record<string, Acceleration> | null> {
|
||||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
if (this.useWebsocket && this.websocketConnected) {
|
||||||
|
return this._accelerations;
|
||||||
|
}
|
||||||
|
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 {
|
||||||
@ -82,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)));
|
||||||
@ -117,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,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();
|
||||||
@ -8,7 +8,15 @@ import { TransactionExtended } from '../../mempool.interfaces';
|
|||||||
interface WalletAddress {
|
interface WalletAddress {
|
||||||
address: string;
|
address: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
transactions?: IEsploraApi.AddressTxSummary[];
|
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 WalletConfig {
|
interface WalletConfig {
|
||||||
@ -22,7 +30,7 @@ interface Wallet extends WalletConfig {
|
|||||||
lastPoll: number;
|
lastPoll: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const POLL_FREQUENCY = 60 * 60 * 1000; // 1 hour
|
const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
class WalletApi {
|
class WalletApi {
|
||||||
private wallets: Record<string, Wallet> = {};
|
private wallets: Record<string, Wallet> = {};
|
||||||
@ -39,8 +47,11 @@ class WalletApi {
|
|||||||
return this.wallets?.[wallet]?.addresses || {};
|
return this.wallets?.[wallet]?.addresses || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// resync wallet addresses from the provided API
|
// resync wallet addresses from the services backend
|
||||||
async $syncWallets(): Promise<void> {
|
async $syncWallets(): Promise<void> {
|
||||||
|
if (!config.WALLETS.ENABLED || this.syncing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.syncing = true;
|
this.syncing = true;
|
||||||
for (const walletKey of Object.keys(this.wallets)) {
|
for (const walletKey of Object.keys(this.wallets)) {
|
||||||
const wallet = this.wallets[walletKey];
|
const wallet = this.wallets[walletKey];
|
||||||
@ -77,10 +88,14 @@ class WalletApi {
|
|||||||
const refreshTransactions = !wallet.addresses[address.address] || address.active;
|
const refreshTransactions = !wallet.addresses[address.address] || address.active;
|
||||||
if (refreshTransactions) {
|
if (refreshTransactions) {
|
||||||
try {
|
try {
|
||||||
|
const summary = await bitcoinApi.$getAddressTransactionSummary(address.address);
|
||||||
|
const addressInfo = await bitcoinApi.$getAddress(address.address);
|
||||||
const walletAddress: WalletAddress = {
|
const walletAddress: WalletAddress = {
|
||||||
address: address.address,
|
address: address.address,
|
||||||
active: address.active,
|
active: address.active,
|
||||||
transactions: await bitcoinApi.$getAddressTransactionSummary(address.address),
|
transactions: await bitcoinApi.$getAddressTransactionSummary(address.address),
|
||||||
|
stats: addressInfo.chain_stats,
|
||||||
|
lastSync: Date.now(),
|
||||||
};
|
};
|
||||||
logger.debug(`Synced ${walletAddress.transactions?.length || 0} transactions for wallet ${wallet.name} address ${address.address}`);
|
logger.debug(`Synced ${walletAddress.transactions?.length || 0} transactions for wallet ${wallet.name} address ${address.address}`);
|
||||||
wallet.addresses[address.address] = walletAddress;
|
wallet.addresses[address.address] = walletAddress;
|
||||||
@ -91,36 +106,61 @@ class WalletApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check a new block for transactions that affect wallet address balances, and add relevant transactions to wallets
|
// 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, Record<string, IEsploraApi.AddressTxSummary[]>> {
|
processBlock(block: IEsploraApi.Block, blockTxs: TransactionExtended[]): Record<string, IEsploraApi.Transaction[]> {
|
||||||
const walletTransactions: Record<string, Record<string, IEsploraApi.AddressTxSummary[]>> = {};
|
const walletTransactions: Record<string, IEsploraApi.Transaction[]> = {};
|
||||||
for (const walletKey of Object.keys(this.wallets)) {
|
for (const walletKey of Object.keys(this.wallets)) {
|
||||||
const wallet = this.wallets[walletKey];
|
const wallet = this.wallets[walletKey];
|
||||||
walletTransactions[walletKey] = {};
|
walletTransactions[walletKey] = [];
|
||||||
for (const tx of blockTxs) {
|
for (const tx of blockTxs) {
|
||||||
const funded: Record<string, number> = {};
|
const funded: Record<string, number> = {};
|
||||||
const spent: 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) {
|
for (const vin of tx.vin) {
|
||||||
const address = vin.prevout?.scriptpubkey_address;
|
const address = vin.prevout?.scriptpubkey_address;
|
||||||
if (address && wallet.addresses[address]) {
|
if (address && wallet.addresses[address]) {
|
||||||
|
anyMatch = true;
|
||||||
spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0);
|
spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0);
|
||||||
|
spentCount[address] = (spentCount[address] ?? 0) + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const vout of tx.vout) {
|
for (const vout of tx.vout) {
|
||||||
const address = vout.scriptpubkey_address;
|
const address = vout.scriptpubkey_address;
|
||||||
if (address && wallet.addresses[address]) {
|
if (address && wallet.addresses[address]) {
|
||||||
|
anyMatch = true;
|
||||||
funded[address] = (funded[address] ?? 0) + (vout.value ?? 0);
|
funded[address] = (funded[address] ?? 0) + (vout.value ?? 0);
|
||||||
|
fundedCount[address] = (fundedCount[address] ?? 0) + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const address of Object.keys({ ...funded, ...spent })) {
|
for (const address of Object.keys({ ...funded, ...spent })) {
|
||||||
if (!walletTransactions[walletKey][address]) {
|
// update address stats
|
||||||
walletTransactions[walletKey][address] = [];
|
wallet.addresses[address].stats.tx_count++;
|
||||||
}
|
wallet.addresses[address].stats.funded_txo_count += fundedCount[address] || 0;
|
||||||
walletTransactions[walletKey][address].push({
|
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,
|
txid: tx.txid,
|
||||||
value: (funded[address] ?? 0) - (spent[address] ?? 0),
|
value: (funded[address] ?? 0) - (spent[address] ?? 0),
|
||||||
height: block.height,
|
height: block.height,
|
||||||
time: block.timestamp,
|
time: block.timestamp,
|
||||||
});
|
};
|
||||||
|
wallet.addresses[address].transactions?.push(txSummary);
|
||||||
|
}
|
||||||
|
if (anyMatch) {
|
||||||
|
for (const address of Object.keys({ ...funded, ...spent })) {
|
||||||
|
if (!walletTransactions[walletKey][address]) {
|
||||||
|
walletTransactions[walletKey][address] = [];
|
||||||
|
}
|
||||||
|
walletTransactions[walletKey][address].push({
|
||||||
|
txid: tx.txid,
|
||||||
|
value: (funded[address] ?? 0) - (spent[address] ?? 0),
|
||||||
|
height: block.height,
|
||||||
|
time: block.timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,4 +168,4 @@ class WalletApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new WalletApi();
|
export default new WalletApi();
|
||||||
|
|||||||
@ -121,6 +121,7 @@ class TransactionUtils {
|
|||||||
const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
|
const adjustedVsize = Math.max(fractionalVsize, sigops * 5); // adjusted vsize = Max(weight, sigops * bytes_per_sigop) / witness_scale_factor
|
||||||
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
|
const feePerVbytes = (transaction.fee || 0) / fractionalVsize;
|
||||||
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
|
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
|
||||||
|
const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes;
|
||||||
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
||||||
order: this.txidToOrdering(transaction.txid),
|
order: this.txidToOrdering(transaction.txid),
|
||||||
vsize,
|
vsize,
|
||||||
@ -128,7 +129,7 @@ class TransactionUtils {
|
|||||||
sigops,
|
sigops,
|
||||||
feePerVsize: feePerVbytes,
|
feePerVsize: feePerVbytes,
|
||||||
adjustedFeePerVsize: adjustedFeePerVsize,
|
adjustedFeePerVsize: adjustedFeePerVsize,
|
||||||
effectiveFeePerVsize: adjustedFeePerVsize,
|
effectiveFeePerVsize: effectiveFeePerVsize,
|
||||||
});
|
});
|
||||||
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
|
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
|
||||||
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
|
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
|
||||||
@ -338,6 +339,87 @@ class TransactionUtils {
|
|||||||
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
||||||
return witness[positionOfScript];
|
return witness[positionOfScript];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calculate the most parsimonious set of prioritizations given a list of block transactions
|
||||||
|
// (i.e. the most likely prioritizations and deprioritizations)
|
||||||
|
public identifyPrioritizedTransactions(transactions: any[], rateKey: string): { prioritized: string[], deprioritized: string[] } {
|
||||||
|
// find the longest increasing subsequence of transactions
|
||||||
|
// (adapted from https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms)
|
||||||
|
// should be O(n log n)
|
||||||
|
const X = transactions.slice(1).reverse().map((tx) => ({ txid: tx.txid, rate: tx[rateKey] })); // standard block order is by *decreasing* effective fee rate, but we want to iterate in increasing order (and skip the coinbase)
|
||||||
|
if (X.length < 2) {
|
||||||
|
return { prioritized: [], deprioritized: [] };
|
||||||
|
}
|
||||||
|
const N = X.length;
|
||||||
|
const P: number[] = new Array(N);
|
||||||
|
const M: number[] = new Array(N + 1);
|
||||||
|
M[0] = -1; // undefined so can be set to any value
|
||||||
|
|
||||||
|
let L = 0;
|
||||||
|
for (let i = 0; i < N; i++) {
|
||||||
|
// Binary search for the smallest positive l ≤ L
|
||||||
|
// such that X[M[l]].effectiveFeePerVsize > X[i].effectiveFeePerVsize
|
||||||
|
let lo = 1;
|
||||||
|
let hi = L + 1;
|
||||||
|
while (lo < hi) {
|
||||||
|
const mid = lo + Math.floor((hi - lo) / 2); // lo <= mid < hi
|
||||||
|
if (X[M[mid]].rate > X[i].rate) {
|
||||||
|
hi = mid;
|
||||||
|
} else { // if X[M[mid]].effectiveFeePerVsize < X[i].effectiveFeePerVsize
|
||||||
|
lo = mid + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After searching, lo == hi is 1 greater than the
|
||||||
|
// length of the longest prefix of X[i]
|
||||||
|
const newL = lo;
|
||||||
|
|
||||||
|
// The predecessor of X[i] is the last index of
|
||||||
|
// the subsequence of length newL-1
|
||||||
|
P[i] = M[newL - 1];
|
||||||
|
M[newL] = i;
|
||||||
|
|
||||||
|
if (newL > L) {
|
||||||
|
// If we found a subsequence longer than any we've
|
||||||
|
// found yet, update L
|
||||||
|
L = newL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct the longest increasing subsequence
|
||||||
|
// It consists of the values of X at the L indices:
|
||||||
|
// ..., P[P[M[L]]], P[M[L]], M[L]
|
||||||
|
const LIS: any[] = new Array(L);
|
||||||
|
let k = M[L];
|
||||||
|
for (let j = L - 1; j >= 0; j--) {
|
||||||
|
LIS[j] = X[k];
|
||||||
|
k = P[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lisMap = new Map<string, number>();
|
||||||
|
LIS.forEach((tx, index) => lisMap.set(tx.txid, index));
|
||||||
|
|
||||||
|
const prioritized: string[] = [];
|
||||||
|
const deprioritized: string[] = [];
|
||||||
|
|
||||||
|
let lastRate = X[0].rate;
|
||||||
|
|
||||||
|
for (const tx of X) {
|
||||||
|
if (lisMap.has(tx.txid)) {
|
||||||
|
lastRate = tx.rate;
|
||||||
|
} else {
|
||||||
|
if (Math.abs(tx.rate - lastRate) < 0.1) {
|
||||||
|
// skip if the rate is almost the same as the previous transaction
|
||||||
|
} else if (tx.rate <= lastRate) {
|
||||||
|
prioritized.push(tx.txid);
|
||||||
|
} else {
|
||||||
|
deprioritized.push(tx.txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { prioritized, deprioritized };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new TransactionUtils();
|
export default new TransactionUtils();
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import * as WebSocket from 'ws';
|
|||||||
import {
|
import {
|
||||||
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
|
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
|
||||||
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
|
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
|
||||||
MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids
|
MempoolDelta, MempoolDeltaTxids
|
||||||
} from '../mempool.interfaces';
|
} from '../mempool.interfaces';
|
||||||
import blocks from './blocks';
|
import blocks from './blocks';
|
||||||
import memPool from './mempool';
|
import memPool from './mempool';
|
||||||
@ -16,11 +16,13 @@ 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';
|
||||||
@ -35,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 = [
|
||||||
@ -58,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) {
|
||||||
@ -493,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');
|
||||||
@ -529,8 +570,17 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param newMempool
|
||||||
|
* @param mempoolSize
|
||||||
|
* @param newTransactions array of transactions added this mempool update.
|
||||||
|
* @param recentlyDeletedTransactions array of arrays of transactions removed in the last N mempool updates, most recent first.
|
||||||
|
* @param accelerationDelta
|
||||||
|
* @param candidates
|
||||||
|
*/
|
||||||
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
|
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
|
||||||
candidates?: GbtCandidates): Promise<void> {
|
candidates?: GbtCandidates): Promise<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');
|
||||||
@ -538,6 +588,8 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
this.printLogs();
|
this.printLogs();
|
||||||
|
|
||||||
|
const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : [];
|
||||||
|
|
||||||
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
|
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
|
||||||
let added = newTransactions;
|
let added = newTransactions;
|
||||||
let removed = deletedTransactions;
|
let removed = deletedTransactions;
|
||||||
@ -556,9 +608,9 @@ class WebsocketHandler {
|
|||||||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||||
const mempoolInfo = memPool.getMempoolInfo();
|
const mempoolInfo = memPool.getMempoolInfo();
|
||||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
|
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;
|
||||||
@ -587,7 +639,7 @@ class WebsocketHandler {
|
|||||||
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
|
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
|
||||||
for (const tx of newTransactions) {
|
for (const tx of newTransactions) {
|
||||||
if (rbfTransactions[tx.txid]) {
|
if (rbfTransactions[tx.txid]) {
|
||||||
for (const replaced of rbfTransactions[tx.txid]) {
|
for (const replaced of rbfTransactions[tx.txid].replaced) {
|
||||||
replacedTransactions.push({ replaced: replaced.txid, by: tx });
|
replacedTransactions.push({ replaced: replaced.txid, by: tx });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -666,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
|
||||||
@ -832,6 +887,7 @@ class WebsocketHandler {
|
|||||||
accelerated: mempoolTx.acceleration || undefined,
|
accelerated: mempoolTx.acceleration || undefined,
|
||||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||||
|
feeDelta: mempoolTx.feeDelta || undefined,
|
||||||
},
|
},
|
||||||
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
|
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
|
||||||
};
|
};
|
||||||
@ -873,6 +929,7 @@ class WebsocketHandler {
|
|||||||
accelerated: mempoolTx.acceleration || undefined,
|
accelerated: mempoolTx.acceleration || undefined,
|
||||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||||
|
feeDelta: mempoolTx.feeDelta || undefined,
|
||||||
};
|
};
|
||||||
if (!mempoolTx.cpfpChecked) {
|
if (!mempoolTx.cpfpChecked) {
|
||||||
calculateMempoolTxCpfp(mempoolTx, newMempool);
|
calculateMempoolTxCpfp(mempoolTx, newMempool);
|
||||||
@ -940,6 +997,8 @@ class WebsocketHandler {
|
|||||||
throw new Error('No WebSocket.Server have been set');
|
throw new Error('No WebSocket.Server have been set');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blockTransactions = structuredClone(transactions);
|
||||||
|
|
||||||
this.printLogs();
|
this.printLogs();
|
||||||
await statistics.runStatistics();
|
await statistics.runStatistics();
|
||||||
|
|
||||||
@ -949,10 +1008,10 @@ class WebsocketHandler {
|
|||||||
let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool);
|
let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool);
|
||||||
|
|
||||||
const accelerations = Object.values(mempool.getAccelerations());
|
const accelerations = Object.values(mempool.getAccelerations());
|
||||||
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, transactions);
|
await accelerationRepository.$indexAccelerationsForBlock(block, accelerations, structuredClone(transactions));
|
||||||
|
|
||||||
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
const rbfTransactions = Common.findMinedRbfTransactions(transactions, memPool.getSpendMap());
|
||||||
memPool.handleMinedRbfTransactions(rbfTransactions);
|
memPool.handleRbfTransactions(rbfTransactions);
|
||||||
memPool.removeFromSpendMap(transactions);
|
memPool.removeFromSpendMap(transactions);
|
||||||
|
|
||||||
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
||||||
@ -969,7 +1028,7 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled()) {
|
||||||
const { censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
const { unseen, censored, added, prioritized, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(block.height, blockTransactions, projectedBlocks, auditMempool);
|
||||||
const matchRate = Math.round(score * 100 * 100) / 100;
|
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||||
|
|
||||||
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
|
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
|
||||||
@ -991,9 +1050,11 @@ class WebsocketHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
BlocksAuditsRepository.$saveAudit({
|
BlocksAuditsRepository.$saveAudit({
|
||||||
|
version: 1,
|
||||||
time: block.timestamp,
|
time: block.timestamp,
|
||||||
height: block.height,
|
height: block.height,
|
||||||
hash: block.id,
|
hash: block.id,
|
||||||
|
unseenTxs: unseen,
|
||||||
addedTxs: added,
|
addedTxs: added,
|
||||||
prioritizedTxs: prioritized,
|
prioritizedTxs: prioritized,
|
||||||
missingTxs: censored,
|
missingTxs: censored,
|
||||||
@ -1020,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
|
||||||
@ -1150,6 +1219,7 @@ class WebsocketHandler {
|
|||||||
accelerated: mempoolTx.acceleration || undefined,
|
accelerated: mempoolTx.acceleration || undefined,
|
||||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||||
|
feeDelta: mempoolTx.feeDelta || undefined,
|
||||||
},
|
},
|
||||||
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
|
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
|
||||||
});
|
});
|
||||||
@ -1172,6 +1242,7 @@ class WebsocketHandler {
|
|||||||
accelerated: mempoolTx.acceleration || undefined,
|
accelerated: mempoolTx.acceleration || undefined,
|
||||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||||
|
feeDelta: mempoolTx.feeDelta || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ interface IConfig {
|
|||||||
AUTOMATIC_POOLS_UPDATE: boolean;
|
AUTOMATIC_POOLS_UPDATE: boolean;
|
||||||
POOLS_JSON_URL: string,
|
POOLS_JSON_URL: string,
|
||||||
POOLS_JSON_TREE_URL: string,
|
POOLS_JSON_TREE_URL: string,
|
||||||
|
POOLS_UPDATE_DELAY: number,
|
||||||
AUDIT: boolean;
|
AUDIT: boolean;
|
||||||
RUST_GBT: boolean;
|
RUST_GBT: boolean;
|
||||||
LIMIT_GBT: boolean;
|
LIMIT_GBT: boolean;
|
||||||
@ -85,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;
|
||||||
@ -200,8 +202,9 @@ const defaults: IConfig = {
|
|||||||
'AUTOMATIC_POOLS_UPDATE': false,
|
'AUTOMATIC_POOLS_UPDATE': false,
|
||||||
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
'POOLS_JSON_URL': 'https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json',
|
||||||
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
'POOLS_JSON_TREE_URL': 'https://api.github.com/repos/mempool/mining-pools/git/trees/master',
|
||||||
|
'POOLS_UPDATE_DELAY': 604800, // in seconds, default is one week
|
||||||
'AUDIT': false,
|
'AUDIT': false,
|
||||||
'RUST_GBT': false,
|
'RUST_GBT': true,
|
||||||
'LIMIT_GBT': false,
|
'LIMIT_GBT': false,
|
||||||
'CPFP_INDEXING': false,
|
'CPFP_INDEXING': false,
|
||||||
'MAX_BLOCKS_BULK_QUERY': 0,
|
'MAX_BLOCKS_BULK_QUERY': 0,
|
||||||
@ -233,7 +236,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',
|
||||||
|
|||||||
@ -213,6 +213,8 @@ class Server {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
poolsUpdater.$startService();
|
||||||
}
|
}
|
||||||
|
|
||||||
async runMainUpdateLoop(): Promise<void> {
|
async runMainUpdateLoop(): Promise<void> {
|
||||||
@ -231,11 +233,11 @@ 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) {
|
if (config.WALLETS.ENABLED) {
|
||||||
@ -316,8 +318,10 @@ 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);
|
bitcoinCoreRoutes.initRoutes(this.app);
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import config from './config';
|
|||||||
import auditReplicator from './replication/AuditReplication';
|
import auditReplicator from './replication/AuditReplication';
|
||||||
import statisticsReplicator from './replication/StatisticsReplication';
|
import statisticsReplicator from './replication/StatisticsReplication';
|
||||||
import AccelerationRepository from './repositories/AccelerationRepository';
|
import AccelerationRepository from './repositories/AccelerationRepository';
|
||||||
|
import BlocksAuditsRepository from './repositories/BlocksAuditsRepository';
|
||||||
|
|
||||||
export interface CoreIndex {
|
export interface CoreIndex {
|
||||||
name: string;
|
name: string;
|
||||||
@ -192,6 +193,7 @@ class Indexer {
|
|||||||
await auditReplicator.$sync();
|
await auditReplicator.$sync();
|
||||||
await statisticsReplicator.$sync();
|
await statisticsReplicator.$sync();
|
||||||
await AccelerationRepository.$indexPastAccelerations();
|
await AccelerationRepository.$indexPastAccelerations();
|
||||||
|
await BlocksAuditsRepository.$migrateAuditsV0toV1();
|
||||||
// do not wait for classify blocks to finish
|
// do not wait for classify blocks to finish
|
||||||
blocks.$classifyBlocks();
|
blocks.$classifyBlocks();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -29,9 +29,11 @@ export interface PoolStats extends PoolInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BlockAudit {
|
export interface BlockAudit {
|
||||||
|
version: number,
|
||||||
time: number,
|
time: number,
|
||||||
height: number,
|
height: number,
|
||||||
hash: string,
|
hash: string,
|
||||||
|
unseenTxs: string[],
|
||||||
missingTxs: string[],
|
missingTxs: string[],
|
||||||
freshTxs: string[],
|
freshTxs: string[],
|
||||||
sigopTxs: string[],
|
sigopTxs: string[],
|
||||||
@ -126,6 +128,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
|||||||
acceleration?: boolean;
|
acceleration?: boolean;
|
||||||
acceleratedBy?: number[];
|
acceleratedBy?: number[];
|
||||||
acceleratedAt?: number;
|
acceleratedAt?: number;
|
||||||
|
feeDelta?: number;
|
||||||
replacement?: boolean;
|
replacement?: boolean;
|
||||||
uid?: number;
|
uid?: number;
|
||||||
flags?: number;
|
flags?: number;
|
||||||
@ -296,6 +299,7 @@ export interface BlockExtension {
|
|||||||
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
|
id: number; // Note - This is the `unique_id`, not to mix with the auto increment `id`
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
minerNames: string[] | null;
|
||||||
};
|
};
|
||||||
avgFee: number;
|
avgFee: number;
|
||||||
avgFeeRate: number;
|
avgFeeRate: number;
|
||||||
@ -316,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;
|
||||||
@ -382,8 +387,9 @@ export interface CpfpCluster {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CpfpSummary {
|
export interface CpfpSummary {
|
||||||
transactions: TransactionExtended[];
|
transactions: MempoolTransactionExtended[];
|
||||||
clusters: CpfpCluster[];
|
clusters: CpfpCluster[];
|
||||||
|
version: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Statistic {
|
export interface Statistic {
|
||||||
@ -449,7 +455,7 @@ export interface OptimizedStatistic {
|
|||||||
|
|
||||||
export interface TxTrackingInfo {
|
export interface TxTrackingInfo {
|
||||||
replacedBy?: string,
|
replacedBy?: string,
|
||||||
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number },
|
position?: { block: number, vsize: number, accelerated?: boolean, acceleratedBy?: number[], acceleratedAt?: number, feeDelta?: number },
|
||||||
cpfp?: {
|
cpfp?: {
|
||||||
ancestors?: Ancestor[],
|
ancestors?: Ancestor[],
|
||||||
bestDescendant?: Ancestor | null,
|
bestDescendant?: Ancestor | null,
|
||||||
@ -462,6 +468,7 @@ export interface TxTrackingInfo {
|
|||||||
accelerated?: boolean,
|
accelerated?: boolean,
|
||||||
acceleratedBy?: number[],
|
acceleratedBy?: number[],
|
||||||
acceleratedAt?: number,
|
acceleratedAt?: number,
|
||||||
|
feeDelta?: number,
|
||||||
confirmed?: boolean
|
confirmed?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,11 +31,11 @@ class AuditReplication {
|
|||||||
const missingAudits = await this.$getMissingAuditBlocks();
|
const missingAudits = await this.$getMissingAuditBlocks();
|
||||||
|
|
||||||
logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication');
|
logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication');
|
||||||
|
|
||||||
let totalSynced = 0;
|
let totalSynced = 0;
|
||||||
let totalMissed = 0;
|
let totalMissed = 0;
|
||||||
let loggerTimer = Date.now();
|
let loggerTimer = Date.now();
|
||||||
// process missing audits in batches of
|
// process missing audits in batches of BATCH_SIZE
|
||||||
for (let i = 0; i < missingAudits.length; i += BATCH_SIZE) {
|
for (let i = 0; i < missingAudits.length; i += BATCH_SIZE) {
|
||||||
const slice = missingAudits.slice(i, i + BATCH_SIZE);
|
const slice = missingAudits.slice(i, i + BATCH_SIZE);
|
||||||
const results = await Promise.all(slice.map(hash => this.$syncAudit(hash)));
|
const results = await Promise.all(slice.map(hash => this.$syncAudit(hash)));
|
||||||
@ -109,9 +109,11 @@ class AuditReplication {
|
|||||||
version: 1,
|
version: 1,
|
||||||
});
|
});
|
||||||
await blocksAuditsRepository.$saveAudit({
|
await blocksAuditsRepository.$saveAudit({
|
||||||
|
version: auditSummary.version || 0,
|
||||||
hash: blockHash,
|
hash: blockHash,
|
||||||
height: auditSummary.height,
|
height: auditSummary.height,
|
||||||
time: auditSummary.timestamp || auditSummary.time,
|
time: auditSummary.timestamp || auditSummary.time,
|
||||||
|
unseenTxs: auditSummary.unseenTxs || [],
|
||||||
missingTxs: auditSummary.missingTxs || [],
|
missingTxs: auditSummary.missingTxs || [],
|
||||||
addedTxs: auditSummary.addedTxs || [],
|
addedTxs: auditSummary.addedTxs || [],
|
||||||
prioritizedTxs: auditSummary.prioritizedTxs || [],
|
prioritizedTxs: auditSummary.prioritizedTxs || [],
|
||||||
|
|||||||
@ -192,6 +192,7 @@ class AccelerationRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// modifies block transactions
|
||||||
public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> {
|
public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> {
|
||||||
const blockTxs: { [txid: string]: MempoolTransactionExtended } = {};
|
const blockTxs: { [txid: string]: MempoolTransactionExtended } = {};
|
||||||
for (const tx of transactions) {
|
for (const tx of transactions) {
|
||||||
|
|||||||
@ -1,13 +1,24 @@
|
|||||||
import blocks from '../api/blocks';
|
|
||||||
import DB from '../database';
|
import DB from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { BlockAudit, AuditScore, TransactionAudit } from '../mempool.interfaces';
|
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
|
||||||
|
import { BlockAudit, AuditScore, TransactionAudit, TransactionStripped } from '../mempool.interfaces';
|
||||||
|
|
||||||
|
interface MigrationAudit {
|
||||||
|
version: number,
|
||||||
|
height: number,
|
||||||
|
id: string,
|
||||||
|
timestamp: number,
|
||||||
|
prioritizedTxs: string[],
|
||||||
|
acceleratedTxs: string[],
|
||||||
|
template: TransactionStripped[],
|
||||||
|
transactions: TransactionStripped[],
|
||||||
|
}
|
||||||
|
|
||||||
class BlocksAuditRepositories {
|
class BlocksAuditRepositories {
|
||||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
|
await DB.query(`INSERT INTO blocks_audits(version, time, height, hash, unseen_txs, missing_txs, added_txs, prioritized_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
|
||||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
VALUE (?, FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.version, audit.time, audit.height, audit.hash, JSON.stringify(audit.unseenTxs), JSON.stringify(audit.missingTxs),
|
||||||
JSON.stringify(audit.addedTxs), JSON.stringify(audit.prioritizedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
|
JSON.stringify(audit.addedTxs), JSON.stringify(audit.prioritizedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||||
@ -62,24 +73,30 @@ class BlocksAuditRepositories {
|
|||||||
public async $getBlockAudit(hash: string): Promise<BlockAudit | null> {
|
public async $getBlockAudit(hash: string): Promise<BlockAudit | null> {
|
||||||
try {
|
try {
|
||||||
const [rows]: any[] = await DB.query(
|
const [rows]: any[] = await DB.query(
|
||||||
`SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
|
`SELECT
|
||||||
template,
|
blocks_audits.version,
|
||||||
missing_txs as missingTxs,
|
blocks_audits.height,
|
||||||
added_txs as addedTxs,
|
blocks_audits.hash as id,
|
||||||
prioritized_txs as prioritizedTxs,
|
UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
|
||||||
fresh_txs as freshTxs,
|
template,
|
||||||
sigop_txs as sigopTxs,
|
unseen_txs as unseenTxs,
|
||||||
fullrbf_txs as fullrbfTxs,
|
missing_txs as missingTxs,
|
||||||
accelerated_txs as acceleratedTxs,
|
added_txs as addedTxs,
|
||||||
match_rate as matchRate,
|
prioritized_txs as prioritizedTxs,
|
||||||
expected_fees as expectedFees,
|
fresh_txs as freshTxs,
|
||||||
expected_weight as expectedWeight
|
sigop_txs as sigopTxs,
|
||||||
|
fullrbf_txs as fullrbfTxs,
|
||||||
|
accelerated_txs as acceleratedTxs,
|
||||||
|
match_rate as matchRate,
|
||||||
|
expected_fees as expectedFees,
|
||||||
|
expected_weight as expectedWeight
|
||||||
FROM blocks_audits
|
FROM blocks_audits
|
||||||
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
|
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
|
||||||
WHERE blocks_audits.hash = ?
|
WHERE blocks_audits.hash = ?
|
||||||
`, [hash]);
|
`, [hash]);
|
||||||
|
|
||||||
if (rows.length) {
|
if (rows.length) {
|
||||||
|
rows[0].unseenTxs = JSON.parse(rows[0].unseenTxs);
|
||||||
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
||||||
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
||||||
rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs);
|
rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs);
|
||||||
@ -101,7 +118,7 @@ class BlocksAuditRepositories {
|
|||||||
public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> {
|
public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> {
|
||||||
try {
|
try {
|
||||||
const blockAudit = await this.$getBlockAudit(hash);
|
const blockAudit = await this.$getBlockAudit(hash);
|
||||||
|
|
||||||
if (blockAudit) {
|
if (blockAudit) {
|
||||||
const isAdded = blockAudit.addedTxs.includes(txid);
|
const isAdded = blockAudit.addedTxs.includes(txid);
|
||||||
const isPrioritized = blockAudit.prioritizedTxs.includes(txid);
|
const isPrioritized = blockAudit.prioritizedTxs.includes(txid);
|
||||||
@ -115,16 +132,17 @@ class BlocksAuditRepositories {
|
|||||||
firstSeen = tx.time;
|
firstSeen = tx.time;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const wasSeen = blockAudit.version === 1 ? !blockAudit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
seen: isExpected || isPrioritized || isAccelerated,
|
seen: wasSeen,
|
||||||
expected: isExpected,
|
expected: isExpected,
|
||||||
added: isAdded,
|
added: isAdded && (blockAudit.version === 0 || !wasSeen),
|
||||||
prioritized: isPrioritized,
|
prioritized: isPrioritized,
|
||||||
conflict: isConflict,
|
conflict: isConflict,
|
||||||
accelerated: isAccelerated,
|
accelerated: isAccelerated,
|
||||||
firstSeen,
|
firstSeen,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@ -186,6 +204,96 @@ class BlocksAuditRepositories {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [INDEXING] Migrate audits from v0 to v1
|
||||||
|
*/
|
||||||
|
public async $migrateAuditsV0toV1(): Promise<void> {
|
||||||
|
try {
|
||||||
|
let done = false;
|
||||||
|
let processed = 0;
|
||||||
|
let lastHeight;
|
||||||
|
while (!done) {
|
||||||
|
const [toMigrate]: MigrationAudit[][] = await DB.query(
|
||||||
|
`SELECT
|
||||||
|
blocks_audits.height as height,
|
||||||
|
blocks_audits.hash as id,
|
||||||
|
UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
|
||||||
|
blocks_summaries.transactions as transactions,
|
||||||
|
blocks_templates.template as template,
|
||||||
|
blocks_audits.prioritized_txs as prioritizedTxs,
|
||||||
|
blocks_audits.accelerated_txs as acceleratedTxs
|
||||||
|
FROM blocks_audits
|
||||||
|
JOIN blocks_summaries ON blocks_summaries.id = blocks_audits.hash
|
||||||
|
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
|
||||||
|
WHERE blocks_audits.version = 0
|
||||||
|
AND blocks_summaries.version = 2
|
||||||
|
ORDER BY blocks_audits.height DESC
|
||||||
|
LIMIT 100
|
||||||
|
`) as any[];
|
||||||
|
|
||||||
|
if (toMigrate.length <= 0 || lastHeight === toMigrate[0].height) {
|
||||||
|
done = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lastHeight = toMigrate[0].height;
|
||||||
|
|
||||||
|
logger.info(`migrating ${toMigrate.length} audits to version 1`);
|
||||||
|
|
||||||
|
for (const audit of toMigrate) {
|
||||||
|
// unpack JSON-serialized transaction lists
|
||||||
|
audit.transactions = JSON.parse((audit.transactions as any as string) || '[]');
|
||||||
|
audit.template = JSON.parse((audit.template as any as string) || '[]');
|
||||||
|
|
||||||
|
// we know transactions in the template, or marked "prioritized" or "accelerated"
|
||||||
|
// were seen in our mempool before the block was mined.
|
||||||
|
const isSeen = new Set<string>();
|
||||||
|
for (const tx of audit.template) {
|
||||||
|
isSeen.add(tx.txid);
|
||||||
|
}
|
||||||
|
for (const txid of audit.prioritizedTxs) {
|
||||||
|
isSeen.add(txid);
|
||||||
|
}
|
||||||
|
for (const txid of audit.acceleratedTxs) {
|
||||||
|
isSeen.add(txid);
|
||||||
|
}
|
||||||
|
const unseenTxs = audit.transactions.slice(0).map(tx => tx.txid).filter(txid => !isSeen.has(txid));
|
||||||
|
|
||||||
|
// identify "prioritized" transactions
|
||||||
|
const prioritizedTxs: string[] = [];
|
||||||
|
let lastEffectiveRate = 0;
|
||||||
|
// Iterate over the mined template from bottom to top (excluding the coinbase)
|
||||||
|
// Transactions should appear in ascending order of mining priority.
|
||||||
|
for (let i = audit.transactions.length - 1; i > 0; i--) {
|
||||||
|
const blockTx = audit.transactions[i];
|
||||||
|
// If a tx has a lower in-band effective fee rate than the previous tx,
|
||||||
|
// it must have been prioritized out-of-band (in order to have a higher mining priority)
|
||||||
|
// so exclude from the analysis.
|
||||||
|
if ((blockTx.rate || 0) < lastEffectiveRate) {
|
||||||
|
prioritizedTxs.push(blockTx.txid);
|
||||||
|
} else {
|
||||||
|
lastEffectiveRate = blockTx.rate || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update audit in the database
|
||||||
|
await DB.query(`
|
||||||
|
UPDATE blocks_audits SET
|
||||||
|
version = ?,
|
||||||
|
unseen_txs = ?,
|
||||||
|
prioritized_txs = ?
|
||||||
|
WHERE hash = ?
|
||||||
|
`, [1, JSON.stringify(unseenTxs), JSON.stringify(prioritizedTxs), audit.id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
processed += toMigrate.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`migrated ${processed} audits to version 1`);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.err(`Error while migrating audits from v0 to v1. Will try again later. Reason: ` + (e instanceof Error ? e.message : e));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new BlocksAuditRepositories();
|
export default new BlocksAuditRepositories();
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import chainTips from '../api/chain-tips';
|
|||||||
import blocks from '../api/blocks';
|
import blocks from '../api/blocks';
|
||||||
import BlocksAuditsRepository from './BlocksAuditsRepository';
|
import BlocksAuditsRepository from './BlocksAuditsRepository';
|
||||||
import transactionUtils from '../api/transaction-utils';
|
import transactionUtils from '../api/transaction-utils';
|
||||||
|
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
|
||||||
|
|
||||||
interface DatabaseBlock {
|
interface DatabaseBlock {
|
||||||
id: string;
|
id: string;
|
||||||
@ -56,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 = `
|
||||||
@ -98,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 {
|
||||||
@ -498,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);
|
||||||
@ -1020,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
|
||||||
@ -1054,6 +1075,7 @@ class BlocksRepository {
|
|||||||
id: dbBlk.poolId,
|
id: dbBlk.poolId,
|
||||||
name: dbBlk.poolName,
|
name: dbBlk.poolName,
|
||||||
slug: dbBlk.poolSlug,
|
slug: dbBlk.poolSlug,
|
||||||
|
minerNames: null,
|
||||||
};
|
};
|
||||||
extras.avgFee = dbBlk.avgFee;
|
extras.avgFee = dbBlk.avgFee;
|
||||||
extras.avgFeeRate = dbBlk.avgFeeRate;
|
extras.avgFeeRate = dbBlk.avgFeeRate;
|
||||||
@ -1076,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
|
||||||
@ -1106,7 +1129,7 @@ class BlocksRepository {
|
|||||||
let summaryVersion = 0;
|
let summaryVersion = 0;
|
||||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
|
const txs = (await bitcoinApi.$getTxsForBlock(dbBlk.id)).map(tx => transactionUtils.extendTransaction(tx));
|
||||||
summary = blocks.summarizeBlockTransactions(dbBlk.id, txs);
|
summary = blocks.summarizeBlockTransactions(dbBlk.id, dbBlk.height, txs);
|
||||||
summaryVersion = 1;
|
summaryVersion = 1;
|
||||||
} else {
|
} else {
|
||||||
// Call Core RPC
|
// Call Core RPC
|
||||||
@ -1123,6 +1146,10 @@ class BlocksRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (extras.pool.name === 'OCEAN') {
|
||||||
|
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
|
||||||
|
}
|
||||||
|
|
||||||
blk.extras = <BlockExtension>extras;
|
blk.extras = <BlockExtension>extras;
|
||||||
return <BlockExtended>blk;
|
return <BlockExtended>blk;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -6,16 +6,30 @@ import backendInfo from '../api/backend-info';
|
|||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
|
import { Common } from '../api/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maintain the most recent version of pools-v2.json
|
* Maintain the most recent version of pools-v2.json
|
||||||
*/
|
*/
|
||||||
class PoolsUpdater {
|
class PoolsUpdater {
|
||||||
|
tag = 'PoolsUpdater';
|
||||||
|
|
||||||
lastRun: number = 0;
|
lastRun: number = 0;
|
||||||
currentSha: string | null = null;
|
currentSha: string | null = null;
|
||||||
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
|
poolsUrl: string = config.MEMPOOL.POOLS_JSON_URL;
|
||||||
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
|
treeUrl: string = config.MEMPOOL.POOLS_JSON_TREE_URL;
|
||||||
|
|
||||||
|
public async $startService(): Promise<void> {
|
||||||
|
while ('Bitcoin is still alive') {
|
||||||
|
try {
|
||||||
|
await this.updatePoolsJson();
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.info(`Exception ${e} in PoolsUpdater::$startService. Code: ${e.code}. Message: ${e.message}`, this.tag);
|
||||||
|
}
|
||||||
|
await Common.sleep$(10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async updatePoolsJson(): Promise<void> {
|
public async updatePoolsJson(): Promise<void> {
|
||||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
|
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
|
||||||
config.MEMPOOL.ENABLED === false
|
config.MEMPOOL.ENABLED === false
|
||||||
@ -23,11 +37,8 @@ class PoolsUpdater {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oneWeek = 604800;
|
|
||||||
const oneDay = 86400;
|
|
||||||
|
|
||||||
const now = new Date().getTime() / 1000;
|
const now = new Date().getTime() / 1000;
|
||||||
if (now - this.lastRun < oneWeek) { // Execute the PoolsUpdate only once a week, or upon restart
|
if (now - this.lastRun < config.MEMPOOL.POOLS_UPDATE_DELAY) { // Execute the PoolsUpdate only once a week, or upon restart
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +54,7 @@ class PoolsUpdater {
|
|||||||
this.currentSha = await this.getShaFromDb();
|
this.currentSha = await this.getShaFromDb();
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`);
|
logger.debug(`pools-v2.json sha | Current: ${this.currentSha} | Github: ${githubSha}`, this.tag);
|
||||||
if (this.currentSha !== null && this.currentSha === githubSha) {
|
if (this.currentSha !== null && this.currentSha === githubSha) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -53,16 +64,16 @@ class PoolsUpdater {
|
|||||||
config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
|
config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
|
||||||
!process.env.npm_config_update_pools // We're not manually updating mining pool
|
!process.env.npm_config_update_pools // We're not manually updating mining pool
|
||||||
) {
|
) {
|
||||||
logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`);
|
logger.warn(`Updated mining pools data is available (${githubSha}) but AUTOMATIC_POOLS_UPDATE is disabled`, this.tag);
|
||||||
logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`);
|
logger.info(`You can update your mining pools using the --update-pools command flag. You may want to clear your nginx cache as well if applicable`, this.tag);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
|
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
|
||||||
if (this.currentSha === null) {
|
if (this.currentSha === null) {
|
||||||
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, logger.tags.mining);
|
logger.info(`Downloading pools-v2.json for the first time from ${this.poolsUrl} over ${network}`, this.tag);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, logger.tags.mining);
|
logger.warn(`pools-v2.json is outdated, fetching latest from ${this.poolsUrl} over ${network}`, this.tag);
|
||||||
}
|
}
|
||||||
const poolsJson = await this.query(this.poolsUrl);
|
const poolsJson = await this.query(this.poolsUrl);
|
||||||
if (poolsJson === undefined) {
|
if (poolsJson === undefined) {
|
||||||
@ -71,7 +82,7 @@ class PoolsUpdater {
|
|||||||
poolsParser.setMiningPools(poolsJson);
|
poolsParser.setMiningPools(poolsJson);
|
||||||
|
|
||||||
if (config.DATABASE.ENABLED === false) { // Don't run db operations
|
if (config.DATABASE.ENABLED === false) { // Don't run db operations
|
||||||
logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`);
|
logger.info(`Mining pools-v2.json (${githubSha}) import completed (no database)`, this.tag);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,14 +92,14 @@ class PoolsUpdater {
|
|||||||
await this.updateDBSha(githubSha);
|
await this.updateDBSha(githubSha);
|
||||||
await DB.query('COMMIT;');
|
await DB.query('COMMIT;');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
|
logger.err(`Could not migrate mining pools, rolling back. Exception: ${JSON.stringify(e)}`, this.tag);
|
||||||
await DB.query('ROLLBACK;');
|
await DB.query('ROLLBACK;');
|
||||||
}
|
}
|
||||||
logger.info(`Mining pools-v2.json (${githubSha}) import completed`);
|
logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
|
this.lastRun = now - 600; // Try again in 10 minutes
|
||||||
logger.err(`PoolsUpdater failed. Will try again in 24h. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
|
logger.err(`PoolsUpdater failed. Will try again in 10 minutes. Exception: ${JSON.stringify(e)}`, this.tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +113,7 @@ class PoolsUpdater {
|
|||||||
await DB.query('DELETE FROM state where name="pools_json_sha"');
|
await DB.query('DELETE FROM state where name="pools_json_sha"');
|
||||||
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
|
await DB.query(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
logger.err('Cannot save github pools-v2.json sha into the db. Reason: ' + (e instanceof Error ? e.message : e), this.tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,7 +126,7 @@ class PoolsUpdater {
|
|||||||
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
|
const [rows]: any[] = await DB.query('SELECT string FROM state WHERE name="pools_json_sha"');
|
||||||
return (rows.length > 0 ? rows[0].string : null);
|
return (rows.length > 0 ? rows[0].string : null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), logger.tags.mining);
|
logger.err('Cannot fetch pools-v2.json sha from db. Reason: ' + (e instanceof Error ? e.message : e), this.tag);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,7 +145,7 @@ class PoolsUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, logger.tags.mining);
|
logger.err(`Cannot find "pools-v2.json" in git tree (${this.treeUrl})`, this.tag);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,7 +197,7 @@ class PoolsUpdater {
|
|||||||
}
|
}
|
||||||
return data.data;
|
return data.data;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e));
|
logger.err('Could not connect to Github. Reason: ' + (e instanceof Error ? e.message : e), this.tag);
|
||||||
retry++;
|
retry++;
|
||||||
}
|
}
|
||||||
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
|
await setDelay(config.MEMPOOL.EXTERNAL_RETRY_INTERVAL);
|
||||||
|
|||||||
9
backend/src/utils/api.ts
Normal file
9
backend/src/utils/api.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
export function handleError(req: Request, res: Response, statusCode: number, errorMessage: string | unknown): void {
|
||||||
|
if (req.accepts('json')) {
|
||||||
|
res.status(statusCode).json({ error: errorMessage });
|
||||||
|
} else {
|
||||||
|
res.status(statusCode).send(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -158,7 +158,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
|||||||
if (!opN) {
|
if (!opN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
|
const n = parseInt(opN.match(/[0-9]+/)?.[0] || '', 10);
|
||||||
@ -178,7 +178,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
|||||||
if (!opM) {
|
if (!opM) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
|
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
|
||||||
@ -200,4 +200,28 @@ export function getVarIntLength(n: number): number {
|
|||||||
} else {
|
} else {
|
||||||
return 9;
|
return 9;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extracts miner names from a DATUM coinbase transaction */
|
||||||
|
export function parseDATUMTemplateCreator(coinbaseRaw: string): string[] | null {
|
||||||
|
let bytes: number[] = [];
|
||||||
|
for (let c = 0; c < coinbaseRaw.length; c += 2) {
|
||||||
|
bytes.push(parseInt(coinbaseRaw.slice(c, c + 2), 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip block height
|
||||||
|
let tagLengthByte = 1 + bytes[0];
|
||||||
|
|
||||||
|
let tagsLength = bytes[tagLengthByte];
|
||||||
|
if (tagsLength == 0x4c) {
|
||||||
|
tagLengthByte += 1;
|
||||||
|
tagsLength = bytes[tagLengthByte];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagStart = tagLengthByte + 1;
|
||||||
|
const tags = bytes.slice(tagStart, tagStart + tagsLength);
|
||||||
|
let tagString = String.fromCharCode(...tags);
|
||||||
|
tagString = tagString.replace('\x00', '');
|
||||||
|
|
||||||
|
return tagString.split('\x0f').map((name) => name.replace(/[^a-zA-Z0-9 ]/g, ''));
|
||||||
}
|
}
|
||||||
58
backend/src/utils/file-read.ts
Normal file
58
backend/src/utils/file-read.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -109,6 +109,7 @@ Below we list all settings from `mempool-config.json` and the corresponding over
|
|||||||
"AUTOMATIC_POOLS_UPDATE": false,
|
"AUTOMATIC_POOLS_UPDATE": false,
|
||||||
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
|
"POOLS_JSON_URL": "https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json",
|
||||||
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
|
||||||
|
"POOLS_UPDATE_DELAY": 604800,
|
||||||
"CPFP_INDEXING": false,
|
"CPFP_INDEXING": false,
|
||||||
"MAX_BLOCKS_BULK_QUERY": 0,
|
"MAX_BLOCKS_BULK_QUERY": 0,
|
||||||
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||||
@ -140,6 +141,7 @@ Corresponding `docker-compose.yml` overrides:
|
|||||||
MEMPOOL_AUTOMATIC_POOLS_UPDATE: ""
|
MEMPOOL_AUTOMATIC_POOLS_UPDATE: ""
|
||||||
MEMPOOL_POOLS_JSON_URL: ""
|
MEMPOOL_POOLS_JSON_URL: ""
|
||||||
MEMPOOL_POOLS_JSON_TREE_URL: ""
|
MEMPOOL_POOLS_JSON_TREE_URL: ""
|
||||||
|
MEMPOOL_POOLS_UPDATE_DELAY: ""
|
||||||
MEMPOOL_CPFP_INDEXING: ""
|
MEMPOOL_CPFP_INDEXING: ""
|
||||||
MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
|
MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
|
||||||
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""
|
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""
|
||||||
|
|||||||
@ -36,6 +36,7 @@
|
|||||||
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
|
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
|
||||||
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
||||||
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
|
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
|
||||||
|
"POOLS_UPDATE_DELAY": __MEMPOOL_POOLS_UPDATE_DELAY__,
|
||||||
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__,
|
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__,
|
||||||
"MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
|
"MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
|
||||||
},
|
},
|
||||||
@ -46,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__",
|
||||||
|
|||||||
@ -29,8 +29,9 @@ __MEMPOOL_STDOUT_LOG_MIN_PRIORITY__=${MEMPOOL_STDOUT_LOG_MIN_PRIORITY:=info}
|
|||||||
__MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false}
|
__MEMPOOL_AUTOMATIC_POOLS_UPDATE__=${MEMPOOL_AUTOMATIC_POOLS_UPDATE:=false}
|
||||||
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
|
__MEMPOOL_POOLS_JSON_URL__=${MEMPOOL_POOLS_JSON_URL:=https://raw.githubusercontent.com/mempool/mining-pools/master/pools-v2.json}
|
||||||
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
|
__MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.github.com/repos/mempool/mining-pools/git/trees/master}
|
||||||
|
__MEMPOOL_POOLS_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800}
|
||||||
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
|
__MEMPOOL_AUDIT__=${MEMPOOL_AUDIT:=false}
|
||||||
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=false}
|
__MEMPOOL_RUST_GBT__=${MEMPOOL_RUST_GBT:=true}
|
||||||
__MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false}
|
__MEMPOOL_LIMIT_GBT__=${MEMPOOL_LIMIT_GBT:=false}
|
||||||
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
||||||
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
|
__MEMPOOL_MAX_BLOCKS_BULK_QUERY__=${MEMPOOL_MAX_BLOCKS_BULK_QUERY:=0}
|
||||||
@ -48,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}
|
||||||
@ -144,7 +146,7 @@ __REPLICATION_STATISTICS_START_TIME__=${REPLICATION_STATISTICS_START_TIME:=14819
|
|||||||
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
||||||
|
|
||||||
# MEMPOOL_SERVICES
|
# MEMPOOL_SERVICES
|
||||||
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
|
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:="https://mempool.space/api/v1/services"}
|
||||||
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
||||||
|
|
||||||
# REDIS
|
# REDIS
|
||||||
@ -187,6 +189,7 @@ sed -i "s!__MEMPOOL_STDOUT_LOG_MIN_PRIORITY__!${__MEMPOOL_STDOUT_LOG_MIN_PRIORIT
|
|||||||
sed -i "s!__MEMPOOL_AUTOMATIC_POOLS_UPDATE__!${__MEMPOOL_AUTOMATIC_POOLS_UPDATE__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_AUTOMATIC_POOLS_UPDATE__!${__MEMPOOL_AUTOMATIC_POOLS_UPDATE__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_POOLS_JSON_URL__!${__MEMPOOL_POOLS_JSON_URL__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g" mempool-config.json
|
||||||
|
sed -i "s!__MEMPOOL_POOLS_UPDATE_DELAY__!${__MEMPOOL_POOLS_UPDATE_DELAY__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_AUDIT__!${__MEMPOOL_AUDIT__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_RUST_GBT__!${__MEMPOOL_RUST_GBT__}!g" mempool-config.json
|
||||||
sed -i "s!__MEMPOOL_LIMIT_GBT__!${__MEMPOOL_LIMIT_GBT__}!g" mempool-config.json
|
sed -i "s!__MEMPOOL_LIMIT_GBT__!${__MEMPOOL_LIMIT_GBT__}!g" mempool-config.json
|
||||||
@ -205,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
|
||||||
|
|||||||
@ -41,7 +41,7 @@ __TESTNET_BLOCK_AUDIT_START_HEIGHT__=${TESTNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
|||||||
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
__SIGNET_BLOCK_AUDIT_START_HEIGHT__=${SIGNET_BLOCK_AUDIT_START_HEIGHT:=0}
|
||||||
__ACCELERATOR__=${ACCELERATOR:=false}
|
__ACCELERATOR__=${ACCELERATOR:=false}
|
||||||
__ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true}
|
__ACCELERATOR_BUTTON__=${ACCELERATOR_BUTTON:=true}
|
||||||
__SERVICES_API__=${SERVICES_API:=false}
|
__SERVICES_API__=${SERVICES_API:=https://mempool.space/api/v1/services}
|
||||||
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
|
__PUBLIC_ACCELERATIONS__=${PUBLIC_ACCELERATIONS:=false}
|
||||||
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
|
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
|
||||||
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
|
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ $ npm run config:defaults:liquid
|
|||||||
|
|
||||||
### 3. Run the Frontend
|
### 3. Run the Frontend
|
||||||
|
|
||||||
_Make sure to use Node.js 16.10 and npm 7._
|
_Make sure to use Node.js 20.x and npm 9.x or newer._
|
||||||
|
|
||||||
Install project dependencies and run the frontend server:
|
Install project dependencies and run the frontend server:
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already.
|
|||||||
|
|
||||||
### 1. Build the Frontend
|
### 1. Build the Frontend
|
||||||
|
|
||||||
_Make sure to use Node.js 16.10 and npm 7._
|
_Make sure to use Node.js 20.x and npm 9.x or newer._
|
||||||
|
|
||||||
Build the frontend:
|
Build the frontend:
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,10 @@
|
|||||||
"translation": "src/locale/messages.fr.xlf",
|
"translation": "src/locale/messages.fr.xlf",
|
||||||
"baseHref": "/fr/"
|
"baseHref": "/fr/"
|
||||||
},
|
},
|
||||||
|
"hr": {
|
||||||
|
"translation": "src/locale/messages.hr.xlf",
|
||||||
|
"baseHref": "/hr/"
|
||||||
|
},
|
||||||
"ja": {
|
"ja": {
|
||||||
"translation": "src/locale/messages.ja.xlf",
|
"translation": "src/locale/messages.ja.xlf",
|
||||||
"baseHref": "/ja/"
|
"baseHref": "/ja/"
|
||||||
|
|||||||
51
frontend/custom-meta-config.json
Normal file
51
frontend/custom-meta-config.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -543,16 +543,7 @@ describe('Mainnet', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get('.alert').should('be.visible');
|
cy.get('.alert-replaced').should('be.visible');
|
||||||
cy.get('.alert').invoke('css', 'width').then((alertWidth) => {
|
|
||||||
cy.get('.container-xl > :nth-child(3)').invoke('css', 'width').should('equal', alertWidth);
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get('.btn-warning').then(getRectangle).then((rectA) => {
|
|
||||||
cy.get('.alert').then(getRectangle).then((rectB) => {
|
|
||||||
expect(areOverlapping(rectA, rectB), 'Confirmations box and RBF alert are overlapping').to.be.false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows RBF transactions properly (desktop)', () => {
|
it('shows RBF transactions properly (desktop)', () => {
|
||||||
|
|||||||
@ -750,7 +750,7 @@
|
|||||||
},
|
},
|
||||||
"backendInfo": {
|
"backendInfo": {
|
||||||
"hostname": "node205.tk7.mempool.space",
|
"hostname": "node205.tk7.mempool.space",
|
||||||
"version": "3.0.0-dev",
|
"version": "3.1.0-dev",
|
||||||
"gitCommit": "abbc8a134",
|
"gitCommit": "abbc8a134",
|
||||||
"lightning": false
|
"lightning": false
|
||||||
},
|
},
|
||||||
|
|||||||
1750
frontend/package-lock.json
generated
1750
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mempool-frontend",
|
"name": "mempool-frontend",
|
||||||
"version": "3.0.0-dev",
|
"version": "3.1.0-dev",
|
||||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||||
"license": "GNU Affero General Public License v3.0",
|
"license": "GNU Affero General Public License v3.0",
|
||||||
"homepage": "https://mempool.space",
|
"homepage": "https://mempool.space",
|
||||||
@ -92,10 +92,10 @@
|
|||||||
"ngx-infinite-scroll": "^17.0.0",
|
"ngx-infinite-scroll": "^17.0.0",
|
||||||
"qrcode": "1.5.1",
|
"qrcode": "1.5.1",
|
||||||
"rxjs": "~7.8.1",
|
"rxjs": "~7.8.1",
|
||||||
"esbuild": "^0.23.0",
|
"esbuild": "^0.24.0",
|
||||||
"tinyify": "^4.0.0",
|
"tinyify": "^4.0.0",
|
||||||
"tlite": "^0.1.9",
|
"tlite": "^0.1.9",
|
||||||
"tslib": "~2.6.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",
|
||||||
@ -115,7 +115,7 @@
|
|||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@cypress/schematic": "^2.5.0",
|
"@cypress/schematic": "^2.5.0",
|
||||||
"@types/cypress": "^1.1.3",
|
"@types/cypress": "^1.1.3",
|
||||||
"cypress": "^13.13.0",
|
"cypress": "^13.15.0",
|
||||||
"cypress-fail-on-console-error": "~5.1.0",
|
"cypress-fail-on-console-error": "~5.1.0",
|
||||||
"cypress-wait-until": "^2.0.1",
|
"cypress-wait-until": "^2.0.1",
|
||||||
"mock-socket": "~9.3.1",
|
"mock-socket": "~9.3.1",
|
||||||
|
|||||||
@ -1,14 +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 '@app/route-guards';
|
||||||
|
|
||||||
const browserWindow = window || {};
|
const browserWindow = window || {};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -21,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: {
|
||||||
@ -44,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 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -59,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 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -82,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 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -102,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: {
|
||||||
@ -125,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 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -137,21 +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',
|
||||||
|
canMatch: [TrackerGuard],
|
||||||
|
runGuardsAndResolvers: 'always',
|
||||||
|
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: 'tracker',
|
path: 'widget/wallet',
|
||||||
data: { networkSpecific: true },
|
|
||||||
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'wallet',
|
|
||||||
children: [],
|
children: [],
|
||||||
component: AddressGroupComponent,
|
component: AddressGroupComponent,
|
||||||
data: {
|
data: {
|
||||||
@ -163,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)
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -210,13 +212,9 @@ 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 },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '**',
|
|
||||||
redirectTo: ''
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||||
@ -227,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: {
|
||||||
@ -250,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 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -262,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: {
|
||||||
@ -283,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)
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -298,16 +296,19 @@ 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 },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '**',
|
|
||||||
redirectTo: ''
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!window['isMempoolSpaceBuild']) {
|
||||||
|
routes.push({
|
||||||
|
path: '**',
|
||||||
|
redirectTo: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forRoot(routes, {
|
imports: [RouterModule.forRoot(routes, {
|
||||||
initialNavigation: 'enabledBlocking',
|
initialNavigation: 'enabledBlocking',
|
||||||
|
|||||||
@ -151,7 +151,7 @@ export const languages: Language[] = [
|
|||||||
{ code: 'fr', name: 'Français' }, // French
|
{ code: 'fr', name: 'Français' }, // French
|
||||||
// { code: 'gl', name: 'Galego' }, // Galician
|
// { code: 'gl', name: 'Galego' }, // Galician
|
||||||
{ code: 'ko', name: '한국어' }, // Korean
|
{ code: 'ko', name: '한국어' }, // Korean
|
||||||
// { code: 'hr', name: 'Hrvatski' }, // Croatian
|
{ code: 'hr', name: 'Hrvatski' }, // Croatian
|
||||||
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
|
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
|
||||||
{ code: 'hi', name: 'हिन्दी' }, // Hindi
|
{ code: 'hi', name: 'हिन्दी' }, // Hindi
|
||||||
{ code: 'ne', name: 'नेपाली' }, // Nepalese
|
{ code: 'ne', name: 'नेपाली' }, // Nepalese
|
||||||
|
|||||||
@ -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({
|
||||||
@ -20,4 +20,4 @@ import { ZoneService } from './services/zone.service';
|
|||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
export class AppServerModule {}
|
export class AppServerModule {}
|
||||||
|
|||||||
@ -2,35 +2,38 @@ 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 { StateService } from './services/state.service';
|
import { OrdApiService } from '@app/services/ord-api.service';
|
||||||
import { CacheService } from './services/cache.service';
|
import { StateService } from '@app/services/state.service';
|
||||||
import { PriceService } from './services/price.service';
|
import { CacheService } from '@app/services/cache.service';
|
||||||
import { EnterpriseService } from './services/enterprise.service';
|
import { PriceService } from '@app/services/price.service';
|
||||||
import { WebsocketService } from './services/websocket.service';
|
import { EnterpriseService } from '@app/services/enterprise.service';
|
||||||
import { AudioService } from './services/audio.service';
|
import { WebsocketService } from '@app/services/websocket.service';
|
||||||
import { PreloadService } from './services/preload.service';
|
import { AudioService } from '@app/services/audio.service';
|
||||||
import { SeoService } from './services/seo.service';
|
import { PreloadService } from '@app/services/preload.service';
|
||||||
import { OpenGraphService } from './services/opengraph.service';
|
import { SeoService } from '@app/services/seo.service';
|
||||||
import { ZoneService } from './services/zone-shim.service';
|
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||||
import { SharedModule } from './shared/shared.module';
|
import { ZoneService } from '@app/services/zone-shim.service';
|
||||||
import { StorageService } from './services/storage.service';
|
import { SharedModule } from '@app/shared/shared.module';
|
||||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
import { StorageService } from '@app/services/storage.service';
|
||||||
import { LanguageService } from './services/language.service';
|
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
|
||||||
import { ThemeService } from './services/theme.service';
|
import { LanguageService } from '@app/services/language.service';
|
||||||
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
|
import { ThemeService } from '@app/services/theme.service';
|
||||||
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
|
import { TimeService } from '@app/services/time.service';
|
||||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
|
||||||
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
|
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
|
||||||
import { AppPreloadingStrategy } from './app.preloading-strategy';
|
import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||||
import { ServicesApiServices } from './services/services-api.service';
|
import { CapAddressPipe } from '@app/shared/pipes/cap-address-pipe/cap-address-pipe';
|
||||||
|
import { AppPreloadingStrategy } from '@app/app.preloading-strategy';
|
||||||
|
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||||
import { DatePipe } from '@angular/common';
|
import { DatePipe } from '@angular/common';
|
||||||
|
|
||||||
const providers = [
|
const providers = [
|
||||||
ElectrsApiService,
|
ElectrsApiService,
|
||||||
|
OrdApiService,
|
||||||
StateService,
|
StateService,
|
||||||
CacheService,
|
CacheService,
|
||||||
PriceService,
|
PriceService,
|
||||||
@ -42,6 +45,7 @@ const providers = [
|
|||||||
EnterpriseService,
|
EnterpriseService,
|
||||||
LanguageService,
|
LanguageService,
|
||||||
ThemeService,
|
ThemeService,
|
||||||
|
TimeService,
|
||||||
ShortenStringPipe,
|
ShortenStringPipe,
|
||||||
FiatShortenerPipe,
|
FiatShortenerPipe,
|
||||||
FiatCurrencyPipe,
|
FiatCurrencyPipe,
|
||||||
|
|||||||
@ -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 },
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@ -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
|
||||||
@ -135,7 +135,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const opN = ops.pop();
|
const opN = ops.pop();
|
||||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
|
const n = parseInt(opN.match(/[0-9]+/)[0], 10);
|
||||||
@ -152,7 +152,7 @@ export function parseMultisigScript(script: string): void | { m: number, n: numb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const opM = ops.pop();
|
const opM = ops.pop();
|
||||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
|
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
|
||||||
@ -303,4 +303,4 @@ export async function calcScriptHash$(script: string): Promise<string> {
|
|||||||
return hashArray
|
return hashArray
|
||||||
.map((bytes) => bytes.toString(16).padStart(2, '0'))
|
.map((bytes) => bytes.toString(16).padStart(2, '0'))
|
||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -53,7 +53,7 @@
|
|||||||
<span>Spiral</span>
|
<span>Spiral</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
|
<a href="https://foundrydigital.com/" target="_blank" title="Foundry">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="76" viewBox="0 0 32 76">
|
<svg xmlns="http://www.w3.org/2000/svg" id="b" data-name="Layer 2" style="zoom: 1;" width="32" height="90" viewBox="0 -5 32 90" class="image">
|
||||||
<defs>
|
<defs>
|
||||||
<style>
|
<style>
|
||||||
.d {
|
.d {
|
||||||
@ -125,17 +125,14 @@
|
|||||||
<span>Blockstream</span>
|
<span>Blockstream</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://unchained.com/" target="_blank" title="Unchained">
|
<a href="https://unchained.com/" target="_blank" title="Unchained">
|
||||||
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68"><defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/></svg>
|
<svg id="Layer_1" width="78" height="78" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.68 156.68" class="image">
|
||||||
|
<defs><style>.cls-unchained-1{fill:#fff;}</style></defs><path class="cls-unchained-1" d="m78.34,0C35.07,0,0,35.07,0,78.34s35.07,78.34,78.34,78.34,78.34-35.07,78.34-78.34S121.6,0,78.34,0ZM20.23,109.5c-4.99-9.28-7.81-19.89-7.81-31.16C12.42,41.93,41.93,12.42,78.34,12.42c33.15,0,60.58,24.46,65.23,56.32h-37.48c-45.29,0-71.19,20.05-85.85,40.76Zm58.11,34.76c-12.42,0-24.04-3.44-33.96-9.41,3.94-8.85,9.11-18.7,15.84-28.9,20.99-31.8,52.2-31.19,76.49-31.19h7.45c.06,1.18.1,2.38.1,3.58,0,36.41-29.51,65.92-65.92,65.92Z"/><path class="cls-unchained-1" d="m91.98,42.4l-3.62-1.18c-3.94-1.29-7.03-4.38-8.32-8.32l-1.18-3.63c-.13-.39-.68-.39-.81,0l-1.18,3.63c-1.29,3.94-4.38,7.03-8.32,8.32l-3.62,1.18c-.39.13-.39.68,0,.81l3.62,1.18c3.94,1.29,7.03,4.38,8.32,8.32l1.18,3.63c.13.39.68.39.81,0l1.18-3.63c1.29-3.94,4.38-7.03,8.32-8.32l3.62-1.18c.39-.13.39-.68,0-.81Z"/>
|
||||||
|
</svg>
|
||||||
<span>Unchained</span>
|
<span>Unchained</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
<a href="https://bitkey.world/" target="_blank" title="Bitkey">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
|
<img class="image" src="/resources/profile/bitkey.svg" />
|
||||||
<rect style="fill: black" width="360" height="360" />
|
<span>Bitkey</span>
|
||||||
<g transform="matrix(0.62 0 0 0.62 180 180)">
|
|
||||||
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<span>Gemini</span>
|
|
||||||
</a>
|
</a>
|
||||||
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
|
<a href="https://bullbitcoin.com/" target="_blank" title="Bull Bitcoin">
|
||||||
<svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
|
<svg aria-hidden="true" class="image" viewBox="0 -5 40 40" xmlns="http://www.w3.org/2000/svg">
|
||||||
@ -150,7 +147,7 @@
|
|||||||
<span>Bull Bitcoin</span>
|
<span>Bull Bitcoin</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
<a href="https://exodus.com/" target="_blank" title="Exodus">
|
||||||
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="80" height="80" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg" class="image">
|
||||||
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
|
<circle cx="250" cy="250" r="250" fill="#1F2033"/>
|
||||||
<g clip-path="url(#clip0_2_14)">
|
<g clip-path="url(#clip0_2_14)">
|
||||||
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
|
<path d="M411.042 178.303L271.79 87V138.048L361.121 196.097L350.612 229.351H271.79V271.648H350.612L361.121 304.903L271.79 362.952V414L411.042 322.989L388.271 250.646L411.042 178.303Z" fill="url(#paint0_linear_2_14)"/>
|
||||||
@ -191,12 +188,30 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>Exodus</span>
|
<span>Exodus</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://gemini.com/" target="_blank" title="Gemini">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="360" height="360" viewBox="0 0 360 360" class="image">
|
||||||
|
<rect style="fill: black" width="360" height="360" />
|
||||||
|
<g transform="matrix(0.62 0 0 0.62 180 180)">
|
||||||
|
<path style="fill: rgb(0,220,250)" transform=" translate(-162, -162)" d="M 211.74 0 C 154.74 0 106.35 43.84 100.25 100.25 C 43.84 106.35 1.4210854715202004e-14 154.76 1.4210854715202004e-14 211.74 C 0.044122601308501076 273.7212006364817 50.27879936351834 323.95587739869154 112.26 324 C 169.26 324 217.84 280.15999999999997 223.75 223.75 C 280.15999999999997 217.65 324 169.24 324 112.26 C 323.95587739869154 50.278799363518324 273.72120063648174 0.04412260130848722 211.74 -1.4210854715202004e-14 z M 297.74 124.84 C 291.9644950552469 162.621439649343 262.2969457716857 192.26062994820046 224.51 198 L 224.51 124.84 z M 26.3 199.16 C 31.986912917108594 161.30935034910615 61.653433460549415 131.56986937804106 99.48999999999998 125.78999999999999 L 99.49 199 L 26.3 199 z M 198.21 224.51 C 191.87736076583954 267.0991541201681 155.312384597087 298.62923417787493 112.255 298.62923417787493 C 69.19761540291302 298.62923417787493 32.63263923416048 267.0991541201682 26.3 224.51 z M 199.16 124.83999999999999 L 199.16 199 L 124.84 199 L 124.84 124.84 z M 297.7 99.48999999999998 L 125.78999999999999 99.48999999999998 C 132.12263923416046 56.90084587983182 168.687615402913 25.37076582212505 211.745 25.37076582212505 C 254.80238459708698 25.37076582212505 291.3673607658395 56.900845879831834 297.7 99.49 z" stroke-linecap="round" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<span>Gemini</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://leather.io/" target="_blank" title="Leather">
|
||||||
|
<img class="image" src="/resources/profile/leather.svg" />
|
||||||
|
<span>Leather</span>
|
||||||
|
</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>
|
||||||
@ -435,7 +450,7 @@
|
|||||||
Trademark Notice<br>
|
Trademark Notice<br>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool logo, the mempool Square logo, the mempool Blocks logo, the mempool Blocks 3 | 2 logo, the mempool.space Vertical Logo, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
The Mempool Open Source Project®, Mempool Accelerator™, Mempool Enterprise®, Mempool Liquidity™, mempool.space®, Be your own explorer™, Explore the full Bitcoin ecosystem®, Mempool Goggles™, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on <https://mempool.space/trademark-policy>.
|
||||||
|
|||||||
@ -13,8 +13,6 @@
|
|||||||
|
|
||||||
.image.not-rounded {
|
.image.not-rounded {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro {
|
.intro {
|
||||||
@ -94,6 +92,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.whale-sponsor {
|
||||||
|
img {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.alliances {
|
.alliances {
|
||||||
margin-bottom: 100px;
|
margin-bottom: 100px;
|
||||||
a {
|
a {
|
||||||
@ -158,9 +163,8 @@
|
|||||||
margin: 40px 29px 10px;
|
margin: 40px 29px 10px;
|
||||||
&.image.coldcard {
|
&.image.coldcard {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
width: auto;
|
height: auto;
|
||||||
max-height: 50px;
|
margin: 20px 29px 20px;
|
||||||
margin: 40px 29px 14px 29px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -254,3 +258,12 @@
|
|||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.enterprise-sponsor {
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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',
|
||||||
|
|||||||
@ -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 = [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -389,21 +389,29 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (canPayWithCashapp || canPayWithApplePay) {
|
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
|
||||||
<div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center">
|
<div class="col-sm text-center flex-grow-0 d-flex flex-column justify-content-center align-items-center">
|
||||||
<p class="text-nowrap">—<span i18n="or">OR</span>—</p>
|
<p class="text-nowrap">—<span i18n="or">OR</span>—</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if (canPayWithCashapp || canPayWithApplePay) {
|
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
|
||||||
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
|
<div class="col-sm text-center d-flex flex-column justify-content-center align-items-center">
|
||||||
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container> <app-fiat [value]="cost"></app-fiat> with</p>
|
<p><ng-container i18n="transaction.pay|Pay button label">Pay</ng-container> <app-fiat [value]="cost"></app-fiat> with</p>
|
||||||
@if (canPayWithCashapp) {
|
@if (canPayWithCashapp) {
|
||||||
<img class="paymentMethod mx-2" style="width: 200px" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')">
|
<img class="paymentMethod mx-2" style="width: 200px" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')">
|
||||||
}
|
}
|
||||||
@if (canPayWithApplePay) {
|
@if (canPayWithApplePay) {
|
||||||
@if (canPayWithCashapp) { <hr class="w-25 mt-2 mb-2"> }
|
@if (canPayWithCashapp) { <span class="mt-1 mb-1"></span> }
|
||||||
<img style="cursor: pointer;" src="/resources/apple-pay.svg" height=55 (click)="moveToStep('applepay')">
|
<div class="paymentMethod mx-2" style="width: 200px; height: 55px" (click)="moveToStep('applepay')">
|
||||||
|
<img src="/resources/apple-pay.png" height=37>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (canPayWithGooglePay) {
|
||||||
|
@if (canPayWithCashapp || canPayWithApplePay) { <span class="mt-1 mb-1"></span> }
|
||||||
|
<div class="paymentMethod mx-2" style="width: 200px; height: 55px" (click)="moveToStep('googlepay')">
|
||||||
|
<img src="/resources/google-pay.png" height=37>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -427,7 +435,7 @@
|
|||||||
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button>
|
<button type="button" class="mt-1 btn btn-secondary btn-sm rounded-pill align-self-center" style="width: 200px" (click)="moveToStep('summary')" i18n="go-back">Go back</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} @else if (step === 'cashapp' || step === 'applepay') {
|
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') {
|
||||||
<!-- Show checkout page -->
|
<!-- Show checkout page -->
|
||||||
<div class="row mb-md-1 text-center" id="confirm-title">
|
<div class="row mb-md-1 text-center" id="confirm-title">
|
||||||
<div class="col-sm" id="confirm-payment-title">
|
<div class="col-sm" id="confirm-payment-title">
|
||||||
@ -443,7 +451,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay) {
|
@if (step === 'cashapp' && !loadingCashapp || step === 'applepay' && !loadingApplePay || step === 'googlepay' && !loadingGooglePay) {
|
||||||
<div class="row text-center mt-1">
|
<div class="row text-center mt-1">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group w-100">
|
<div class="form-group w-100">
|
||||||
@ -463,11 +471,13 @@
|
|||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group w-100">
|
<div class="form-group w-100">
|
||||||
@if (step === 'applepay') {
|
@if (step === 'applepay') {
|
||||||
<div id="apple-pay-button" class="apple-pay-button apple-pay-button-white" [style]="loadingApplePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
<div id="apple-pay-button" class="apple-pay-button apple-pay-button-black" style="height: 50px" [style]="loadingApplePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||||
} @else if (step === 'cashapp') {
|
} @else if (step === 'cashapp') {
|
||||||
<div id="cash-app-pay" class="d-inline-block" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
<div id="cash-app-pay" class="d-inline-block" style="height: 50px" [style]="loadingCashapp ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||||
|
} @else if (step === 'googlepay') {
|
||||||
|
<div id="google-pay-button" class="d-inline-block" style="height: 50px" [style]="loadingGooglePay ? 'opacity: 0; width: 0px; height: 0px; pointer-events: none;' : ''"></div>
|
||||||
}
|
}
|
||||||
@if (loadingCashapp || loadingApplePay) {
|
@if (loadingCashapp || loadingApplePay || loadingGooglePay) {
|
||||||
<div display="d-flex flex-row justify-content-center">
|
<div display="d-flex flex-row justify-content-center">
|
||||||
<span i18n="accelerator.loading-payment-method">Loading payment method...</span>
|
<span i18n="accelerator.loading-payment-method">Loading payment method...</span>
|
||||||
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
||||||
@ -515,7 +525,7 @@
|
|||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="d-flex flex-row flex-column justify-content-center align-items-center">
|
<div class="d-flex flex-row flex-column justify-content-center align-items-center">
|
||||||
<span i18n="accelerator.confirming-acceleration-with-miners">Confirming your acceleration with our mining pool partners...</span>
|
<span i18n="accelerator.confirming-acceleration-with-miners">Confirming your acceleration with our mining pool partners...</span>
|
||||||
@if (timeSincePaid > 20000) {
|
@if (timeSincePaid > 30000) {
|
||||||
<span i18n="accelerator.confirming-acceleration-with-miners">...sorry, this is taking longer than expected...</span>
|
<span i18n="accelerator.confirming-acceleration-with-miners">...sorry, this is taking longer than expected...</span>
|
||||||
}
|
}
|
||||||
<div class="m-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
<div class="m-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -1,17 +1,19 @@
|
|||||||
|
/* 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, nextRoundNumber, 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';
|
||||||
|
|
||||||
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp';
|
export type PaymentMethod = 'balance' | 'bitcoin' | 'cashapp' | 'applePay' | 'googlePay';
|
||||||
|
|
||||||
export type AccelerationEstimate = {
|
export type AccelerationEstimate = {
|
||||||
hasAccess: boolean;
|
hasAccess: boolean;
|
||||||
@ -24,7 +26,7 @@ export type AccelerationEstimate = {
|
|||||||
mempoolBaseFee: number;
|
mempoolBaseFee: number;
|
||||||
vsizeFee: number;
|
vsizeFee: number;
|
||||||
pools: number[];
|
pools: number[];
|
||||||
availablePaymentMethods: {[method: string]: {min: number, max: number}};
|
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number}>;
|
||||||
unavailable?: boolean;
|
unavailable?: boolean;
|
||||||
options: { // recommended bid options
|
options: { // recommended bid options
|
||||||
fee: number; // recommended userBid in sats
|
fee: number; // recommended userBid in sats
|
||||||
@ -47,7 +49,7 @@ export const MIN_BID_RATIO = 1;
|
|||||||
export const DEFAULT_BID_RATIO = 2;
|
export const DEFAULT_BID_RATIO = 2;
|
||||||
export const MAX_BID_RATIO = 4;
|
export const MAX_BID_RATIO = 4;
|
||||||
|
|
||||||
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'processing' | 'paid' | 'success';
|
type CheckoutStep = 'quote' | 'summary' | 'checkout' | 'cashapp' | 'applepay' | 'googlepay' | 'processing' | 'paid' | 'success';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-accelerate-checkout',
|
selector: 'app-accelerate-checkout',
|
||||||
@ -62,6 +64,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
@Input() scrollEvent: boolean;
|
@Input() scrollEvent: boolean;
|
||||||
@Input() cashappEnabled: boolean = true;
|
@Input() cashappEnabled: boolean = true;
|
||||||
@Input() applePayEnabled: boolean = false;
|
@Input() applePayEnabled: boolean = false;
|
||||||
|
@Input() googlePayEnabled: boolean = true;
|
||||||
@Input() advancedEnabled: boolean = false;
|
@Input() advancedEnabled: boolean = false;
|
||||||
@Input() forceMobile: boolean = false;
|
@Input() forceMobile: boolean = false;
|
||||||
@Input() showDetails: boolean = false;
|
@Input() showDetails: boolean = false;
|
||||||
@ -72,6 +75,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
@Output() changeMode = new EventEmitter<boolean>();
|
@Output() changeMode = new EventEmitter<boolean>();
|
||||||
|
|
||||||
calculating = true;
|
calculating = true;
|
||||||
|
processing = false;
|
||||||
selectedOption: 'wait' | 'accel';
|
selectedOption: 'wait' | 'accel';
|
||||||
cantPayReason = '';
|
cantPayReason = '';
|
||||||
quoteError = ''; // error fetching estimate or initial data
|
quoteError = ''; // error fetching estimate or initial data
|
||||||
@ -80,18 +84,16 @@ 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 = false;
|
||||||
|
|
||||||
private _step: CheckoutStep = 'summary';
|
private _step: CheckoutStep = 'summary';
|
||||||
simpleMode: boolean = true;
|
simpleMode: boolean = true;
|
||||||
paymentMethod: 'cashapp' | 'btcpay';
|
|
||||||
timeoutTimer: any;
|
timeoutTimer: any;
|
||||||
|
|
||||||
authSubscription$: Subscription;
|
authSubscription$: Subscription;
|
||||||
auth: IAuth | null = null;
|
auth: IAuth | null = null;
|
||||||
|
|
||||||
// accelerator stuff
|
// accelerator stuff
|
||||||
square: { appId: string, locationId: string};
|
|
||||||
accelerationUUID: string;
|
|
||||||
accelerationSubscription: Subscription;
|
accelerationSubscription: Subscription;
|
||||||
difficultySubscription: Subscription;
|
difficultySubscription: Subscription;
|
||||||
estimateSubscription: Subscription;
|
estimateSubscription: Subscription;
|
||||||
@ -112,14 +114,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
// square
|
// square
|
||||||
loadingCashapp = false;
|
loadingCashapp = false;
|
||||||
loadingApplePay = false;
|
loadingApplePay = false;
|
||||||
cashappError = false;
|
loadingGooglePay = false;
|
||||||
cashappSubmit: any;
|
|
||||||
payments: any;
|
payments: any;
|
||||||
cashAppPay: any;
|
cashAppPay: any;
|
||||||
applePay: any;
|
applePay: any;
|
||||||
|
googlePay: any;
|
||||||
conversionsSubscription: Subscription;
|
conversionsSubscription: Subscription;
|
||||||
conversions: any;
|
conversions: Record<string, number>;
|
||||||
|
|
||||||
// btcpay
|
// btcpay
|
||||||
loadingBtcpayInvoice = false;
|
loadingBtcpayInvoice = false;
|
||||||
invoice = undefined;
|
invoice = undefined;
|
||||||
@ -134,16 +136,16 @@ 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
|
||||||
// @ts-ignore 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
|
||||||
if (window.ApplePaySession) {
|
if (window['ApplePaySession']) {
|
||||||
this.applePayEnabled = true;
|
this.applePayEnabled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
this.authSubscription$ = this.authService.getAuth$().subscribe((auth) => {
|
this.authSubscription$ = this.authService.getAuth$().subscribe((auth) => {
|
||||||
if (this.auth?.user?.userId !== auth?.user?.userId) {
|
if (this.auth?.user?.userId !== auth?.user?.userId) {
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
@ -168,13 +170,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.moveToStep('summary');
|
this.moveToStep('summary');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.servicesApiService.setupSquare$().subscribe(ids => {
|
|
||||||
this.square = {
|
|
||||||
appId: ids.squareAppId,
|
|
||||||
locationId: ids.squareLocationId
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||||
async (conversions) => {
|
async (conversions) => {
|
||||||
this.conversions = conversions;
|
this.conversions = conversions;
|
||||||
@ -182,7 +177,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy(): void {
|
||||||
if (this.estimateSubscription) {
|
if (this.estimateSubscription) {
|
||||||
this.estimateSubscription.unsubscribe();
|
this.estimateSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
@ -195,14 +190,17 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
if (changes.scrollEvent && this.scrollEvent) {
|
if (changes.scrollEvent && this.scrollEvent) {
|
||||||
this.scrollToElement('acceleratePreviewAnchor', 'start');
|
this.scrollToElement('acceleratePreviewAnchor', 'start');
|
||||||
}
|
}
|
||||||
if (changes.accelerating) {
|
if (changes.accelerating && this.accelerating) {
|
||||||
if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) {
|
if (this.step === 'processing' || this.step === 'paid') {
|
||||||
this.moveToStep('success');
|
this.moveToStep('success');
|
||||||
|
} else { // Edge case where the transaction gets accelerated by someone else or on another session
|
||||||
|
this.closeModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
moveToStep(step: CheckoutStep) {
|
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);
|
||||||
@ -211,6 +209,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.fetchEstimate();
|
this.fetchEstimate();
|
||||||
}
|
}
|
||||||
if (this._step === 'checkout') {
|
if (this._step === 'checkout') {
|
||||||
|
this.insertSquare();
|
||||||
this.enterpriseService.goal(8);
|
this.enterpriseService.goal(8);
|
||||||
}
|
}
|
||||||
if (this._step === 'checkout' && this.canPayWithBitcoin) {
|
if (this._step === 'checkout' && this.canPayWithBitcoin) {
|
||||||
@ -220,12 +219,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.requestBTCPayInvoice();
|
this.requestBTCPayInvoice();
|
||||||
} else if (this._step === 'cashapp' && this.cashappEnabled) {
|
} else if (this._step === 'cashapp' && this.cashappEnabled) {
|
||||||
this.loadingCashapp = true;
|
this.loadingCashapp = true;
|
||||||
this.insertSquare();
|
|
||||||
this.setupSquare();
|
this.setupSquare();
|
||||||
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||||
} else if (this._step === 'applepay' && this.applePayEnabled) {
|
} else if (this._step === 'applepay' && this.applePayEnabled) {
|
||||||
this.loadingApplePay = true;
|
this.loadingApplePay = true;
|
||||||
this.insertSquare();
|
this.setupSquare();
|
||||||
|
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||||
|
} else if (this._step === 'googlepay' && this.googlePayEnabled) {
|
||||||
|
this.loadingGooglePay = true;
|
||||||
this.setupSquare();
|
this.setupSquare();
|
||||||
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||||
} else if (this._step === 'paid') {
|
} else if (this._step === 'paid') {
|
||||||
@ -234,7 +235,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
if (this.step === 'paid') {
|
if (this.step === 'paid') {
|
||||||
this.accelerateError = 'internal_server_error';
|
this.accelerateError = 'internal_server_error';
|
||||||
}
|
}
|
||||||
}, 120000)
|
}, 120000);
|
||||||
}
|
}
|
||||||
this.hasDetails.emit(this._step === 'quote');
|
this.hasDetails.emit(this._step === 'quote');
|
||||||
}
|
}
|
||||||
@ -252,7 +253,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.scrollToElement(id, position);
|
this.scrollToElement(id, position);
|
||||||
}, timeout);
|
}, timeout);
|
||||||
}
|
}
|
||||||
scrollToElement(id: string, position: ScrollLogicalPosition) {
|
scrollToElement(id: string, position: ScrollLogicalPosition): void {
|
||||||
const acceleratePreviewAnchor = document.getElementById(id);
|
const acceleratePreviewAnchor = document.getElementById(id);
|
||||||
if (acceleratePreviewAnchor) {
|
if (acceleratePreviewAnchor) {
|
||||||
this.cd.markForCheck();
|
this.cd.markForCheck();
|
||||||
@ -267,7 +268,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Accelerator
|
* Accelerator
|
||||||
*/
|
*/
|
||||||
fetchEstimate() {
|
fetchEstimate(): void {
|
||||||
if (this.estimateSubscription) {
|
if (this.estimateSubscription) {
|
||||||
this.estimateSubscription.unsubscribe();
|
this.estimateSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
@ -331,7 +332,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
catchError((response) => {
|
catchError(() => {
|
||||||
this.estimate = undefined;
|
this.estimate = undefined;
|
||||||
this.quoteError = `cannot_accelerate_tx`;
|
this.quoteError = `cannot_accelerate_tx`;
|
||||||
this.estimateSubscription.unsubscribe();
|
this.estimateSubscription.unsubscribe();
|
||||||
@ -367,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -374,18 +376,19 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
* Account-based acceleration request
|
* Account-based acceleration request
|
||||||
*/
|
*/
|
||||||
accelerateWithMempoolAccount(): void {
|
accelerateWithMempoolAccount(): void {
|
||||||
if (!this.canPay || this.calculating) {
|
if (!this.canPay || this.calculating || this.processing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.processing = true;
|
||||||
if (this.accelerationSubscription) {
|
if (this.accelerationSubscription) {
|
||||||
this.accelerationSubscription.unsubscribe();
|
this.accelerationSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
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.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
this.showSuccess = true;
|
this.showSuccess = true;
|
||||||
@ -393,6 +396,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
this.moveToStep('paid');
|
this.moveToStep('paid');
|
||||||
},
|
},
|
||||||
error: (response) => {
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
this.accelerateError = response.error;
|
this.accelerateError = response.error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -402,63 +406,74 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
* Square
|
* Square
|
||||||
*/
|
*/
|
||||||
insertSquare(): void {
|
insertSquare(): void {
|
||||||
//@ts-ignore
|
if (!this.isProdDomain && !isDevMode()) {
|
||||||
if (window.Square) {
|
return;
|
||||||
|
}
|
||||||
|
if (window['Square']) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js';
|
let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js';
|
||||||
if (document.location.hostname === 'mempool-staging.fmt.mempool.space' ||
|
if (this.isProdDomain) {
|
||||||
document.location.hostname === 'mempool-staging.va1.mempool.space' ||
|
statsUrl = '/square/v1/square.js';
|
||||||
document.location.hostname === 'mempool-staging.fra.mempool.space' ||
|
|
||||||
document.location.hostname === 'mempool-staging.tk7.mempool.space' ||
|
|
||||||
document.location.hostname === 'mempool.space') {
|
|
||||||
statsUrl = 'https://web.squarecdn.com/v1/square.js';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(function() {
|
(function(): void {
|
||||||
const d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
const d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
|
||||||
// @ts-ignore
|
|
||||||
g.type='text/javascript'; g.src=statsUrl; s.parentNode.insertBefore(g, s);
|
g.type='text/javascript'; g.src=statsUrl; s.parentNode.insertBefore(g, s);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
setupSquare() {
|
setupSquare(): void {
|
||||||
const init = () => {
|
if (!this.isProdDomain && !isDevMode()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const init = (): void => {
|
||||||
this.initSquare();
|
this.initSquare();
|
||||||
};
|
};
|
||||||
|
|
||||||
//@ts-ignore
|
if (!window['Square']) {
|
||||||
if (!window.Square) {
|
console.debug('Square.js failed to load properly. Retrying.');
|
||||||
console.debug('Square.js failed to load properly. Retrying in 1 second.');
|
setTimeout(this.setupSquare.bind(this), 100);
|
||||||
setTimeout(init, 1000);
|
|
||||||
} else {
|
} else {
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async initSquare(): Promise<void> {
|
async initSquare(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
//@ts-ignore
|
this.servicesApiService.setupSquare$().subscribe({
|
||||||
this.payments = window.Square.payments(this.square.appId, this.square.locationId)
|
next: async (ids) => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
this.payments = window['Square'].payments(ids.squareAppId, ids.squareLocationId);
|
||||||
if (this._step === 'cashapp' || urlParams.get('cash_request_id')) {
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
await this.requestCashAppPayment();
|
if (this._step === 'cashapp' || urlParams.get('cash_request_id')) {
|
||||||
} else if (this._step === 'applepay') {
|
await this.requestCashAppPayment();
|
||||||
await this.requestApplePayPayment();
|
} else if (this._step === 'applepay') {
|
||||||
}
|
await this.requestApplePayPayment();
|
||||||
|
} else if (this._step === 'googlepay') {
|
||||||
|
await this.requestGooglePayPayment();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
console.debug('Error loading Square Payments');
|
||||||
|
this.accelerateError = 'cannot_setup_square';
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.debug('Error loading Square Payments', e);
|
console.debug('Error loading Square Payments', e);
|
||||||
this.cashappError = true;
|
this.accelerateError = 'cannot_setup_square';
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* APPLE PAY
|
* APPLE PAY
|
||||||
*/
|
*/
|
||||||
async requestApplePayPayment() {
|
async requestApplePayPayment(): Promise<void> {
|
||||||
|
if (this.processing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.conversionsSubscription) {
|
if (this.conversionsSubscription) {
|
||||||
this.conversionsSubscription.unsubscribe();
|
this.conversionsSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.processing = true;
|
||||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||||
async (conversions) => {
|
async (conversions) => {
|
||||||
this.conversions = conversions;
|
this.conversions = conversions;
|
||||||
@ -483,6 +498,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
console.error(`Unable to find apple pay button id='apple-pay-button'`);
|
console.error(`Unable to find apple pay button id='apple-pay-button'`);
|
||||||
// Try again
|
// Try again
|
||||||
setTimeout(this.requestApplePayPayment.bind(this), 500);
|
setTimeout(this.requestApplePayPayment.bind(this), 500);
|
||||||
|
this.processing = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loadingApplePay = false;
|
this.loadingApplePay = false;
|
||||||
@ -494,6 +510,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||||
console.error(`Cannot retreive payment card details`);
|
console.error(`Cannot retreive payment card details`);
|
||||||
this.accelerateError = 'apple_pay_no_card_details';
|
this.accelerateError = 'apple_pay_no_card_details';
|
||||||
|
this.processing = false;
|
||||||
return;
|
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());
|
||||||
@ -502,9 +519,11 @@ 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.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
if (this.applePay) {
|
if (this.applePay) {
|
||||||
this.applePay.destroy();
|
this.applePay.destroy();
|
||||||
@ -514,6 +533,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
error: (response) => {
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
this.accelerateError = response.error;
|
this.accelerateError = response.error;
|
||||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -525,6 +545,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
this.processing = false;
|
||||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||||
if (tokenResult.errors) {
|
if (tokenResult.errors) {
|
||||||
errorMessage += ` and errors: ${JSON.stringify(
|
errorMessage += ` and errors: ${JSON.stringify(
|
||||||
@ -535,6 +556,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this.processing = false;
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -542,13 +564,112 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CASHAPP
|
* GOOGLE PAY
|
||||||
*/
|
*/
|
||||||
async requestCashAppPayment() {
|
async requestGooglePayPayment(): Promise<void> {
|
||||||
|
if (this.processing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.conversionsSubscription) {
|
if (this.conversionsSubscription) {
|
||||||
this.conversionsSubscription.unsubscribe();
|
this.conversionsSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.processing = true;
|
||||||
|
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||||
|
async (conversions) => {
|
||||||
|
this.conversions = conversions;
|
||||||
|
if (this.googlePay) {
|
||||||
|
this.googlePay.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const costUSD = this.cost / 100_000_000 * conversions.USD;
|
||||||
|
const paymentRequest = this.payments.paymentRequest({
|
||||||
|
countryCode: 'US',
|
||||||
|
currencyCode: 'USD',
|
||||||
|
total: {
|
||||||
|
amount: costUSD.toFixed(2),
|
||||||
|
label: 'Total'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.googlePay = await this.payments.googlePay(paymentRequest , {
|
||||||
|
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.googlePay.attach(`#google-pay-button`, {
|
||||||
|
buttonType: 'pay',
|
||||||
|
buttonSizeMode: 'fill',
|
||||||
|
});
|
||||||
|
this.loadingGooglePay = false;
|
||||||
|
|
||||||
|
document.getElementById('google-pay-button').addEventListener('click', async event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const tokenResult = await this.googlePay.tokenize();
|
||||||
|
if (tokenResult?.status === 'OK') {
|
||||||
|
const card = tokenResult.details?.card;
|
||||||
|
if (!card || !card.brand || !card.expMonth || !card.expYear || !card.last4) {
|
||||||
|
console.error(`Cannot retreive payment card details`);
|
||||||
|
this.accelerateError = 'apple_pay_no_card_details';
|
||||||
|
this.processing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cardTag = md5(`${card.brand}${card.expMonth}${card.expYear}${card.last4}`.toLowerCase());
|
||||||
|
this.servicesApiService.accelerateWithGooglePay$(
|
||||||
|
this.tx.txid,
|
||||||
|
tokenResult.token,
|
||||||
|
cardTag,
|
||||||
|
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||||
|
costUSD
|
||||||
|
).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.processing = false;
|
||||||
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
|
if (this.googlePay) {
|
||||||
|
this.googlePay.destroy();
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.moveToStep('paid');
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
|
this.accelerateError = response.error;
|
||||||
|
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
// Reset everything by reloading the page :D, can be improved
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
window.location.assign(window.location.toString().replace(`?cash_request_id=${urlParams.get('cash_request_id')}`, ``));
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.processing = false;
|
||||||
|
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||||
|
if (tokenResult.errors) {
|
||||||
|
errorMessage += ` and errors: ${JSON.stringify(
|
||||||
|
tokenResult.errors,
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CASHAPP
|
||||||
|
*/
|
||||||
|
async requestCashAppPayment(): Promise<void> {
|
||||||
|
if (this.processing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.conversionsSubscription) {
|
||||||
|
this.conversionsSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.processing = true;
|
||||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||||
async (conversions) => {
|
async (conversions) => {
|
||||||
this.conversions = conversions;
|
this.conversions = conversions;
|
||||||
@ -565,24 +686,21 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
amount: costUSD.toFixed(2),
|
amount: costUSD.toFixed(2),
|
||||||
label: 'Total',
|
label: 'Total',
|
||||||
pending: true,
|
pending: true,
|
||||||
productUrl: `${redirectHostname}/tracker/${this.tx.txid}`,
|
productUrl: `${redirectHostname}/tx/${this.tx.txid}`,
|
||||||
},
|
}
|
||||||
button: { shape: 'semiround', size: 'small', theme: 'light'}
|
|
||||||
});
|
});
|
||||||
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
|
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
|
||||||
redirectURL: `${redirectHostname}/tracker/${this.tx.txid}`,
|
redirectURL: `${redirectHostname}/tx/${this.tx.txid}`,
|
||||||
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`
|
||||||
button: { shape: 'semiround', size: 'small', theme: 'light'}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.step === 'cashapp') {
|
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'dark' });
|
||||||
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'light', size: 'small', shape: 'semiround' })
|
|
||||||
}
|
|
||||||
this.loadingCashapp = false;
|
this.loadingCashapp = false;
|
||||||
|
|
||||||
this.cashAppPay.addEventListener('ontokenization', event => {
|
this.cashAppPay.addEventListener('ontokenization', event => {
|
||||||
const { tokenResult, error } = event.detail;
|
const { tokenResult, error } = event.detail;
|
||||||
if (error) {
|
if (error) {
|
||||||
|
this.processing = false;
|
||||||
this.accelerateError = error;
|
this.accelerateError = error;
|
||||||
} else if (tokenResult.status === 'OK') {
|
} else if (tokenResult.status === 'OK') {
|
||||||
this.servicesApiService.accelerateWithCashApp$(
|
this.servicesApiService.accelerateWithCashApp$(
|
||||||
@ -590,9 +708,10 @@ 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.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||||
this.audioService.playSound('ascend-chime-cartoon');
|
this.audioService.playSound('ascend-chime-cartoon');
|
||||||
if (this.cashAppPay) {
|
if (this.cashAppPay) {
|
||||||
@ -607,6 +726,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
error: (response) => {
|
error: (response) => {
|
||||||
|
this.processing = false;
|
||||||
this.accelerateError = response.error;
|
this.accelerateError = response.error;
|
||||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -626,7 +746,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* BTCPay
|
* BTCPay
|
||||||
*/
|
*/
|
||||||
async requestBTCPayInvoice() {
|
async requestBTCPayInvoice(): Promise<void> {
|
||||||
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
|
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
|
||||||
switchMap(response => {
|
switchMap(response => {
|
||||||
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
|
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
|
||||||
@ -656,54 +776,61 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* UI events
|
* UI events
|
||||||
*/
|
*/
|
||||||
selectedOptionChanged(event) {
|
selectedOptionChanged(event): void {
|
||||||
this.selectedOption = event.target.id;
|
this.selectedOption = event.target.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get step() {
|
get step(): CheckoutStep {
|
||||||
return this._step;
|
return this._step;
|
||||||
}
|
}
|
||||||
|
|
||||||
get paymentMethods() {
|
get paymentMethods(): PaymentMethod[] {
|
||||||
return Object.keys(this.estimate?.availablePaymentMethods || {});
|
return Object.keys(this.estimate?.availablePaymentMethods || {}) as PaymentMethod[];
|
||||||
}
|
}
|
||||||
|
|
||||||
get couldPayWithBitcoin() {
|
get couldPayWithBitcoin(): boolean {
|
||||||
return !!this.estimate?.availablePaymentMethods?.bitcoin;
|
return !!this.estimate?.availablePaymentMethods?.bitcoin;
|
||||||
}
|
}
|
||||||
|
|
||||||
get couldPayWithCashapp() {
|
get couldPayWithCashapp(): boolean {
|
||||||
if (!this.cashappEnabled) {
|
if (!this.cashappEnabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !!this.estimate?.availablePaymentMethods?.cashapp;
|
return !!this.estimate?.availablePaymentMethods?.cashapp;
|
||||||
}
|
}
|
||||||
|
|
||||||
get couldPayWithApplePay() {
|
get couldPayWithApplePay(): boolean {
|
||||||
if (!this.applePayEnabled) {
|
if (!this.applePayEnabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !!this.estimate?.availablePaymentMethods?.applePay;
|
return !!this.estimate?.availablePaymentMethods?.applePay;
|
||||||
}
|
}
|
||||||
|
|
||||||
get couldPayWithBalance() {
|
get couldPayWithGooglePay(): boolean {
|
||||||
|
if (!this.googlePayEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !!this.estimate?.availablePaymentMethods?.googlePay;
|
||||||
|
}
|
||||||
|
|
||||||
|
get couldPayWithBalance(): boolean {
|
||||||
if (!this.hasAccessToBalanceMode) {
|
if (!this.hasAccessToBalanceMode) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return !!this.estimate?.availablePaymentMethods?.balance;
|
return !!this.estimate?.availablePaymentMethods?.balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
get couldPay() {
|
get couldPay(): boolean {
|
||||||
return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp || this.couldPayWithApplePay;
|
return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp || this.couldPayWithApplePay || this.couldPayWithGooglePay;
|
||||||
}
|
}
|
||||||
|
|
||||||
get canPayWithBitcoin() {
|
get canPayWithBitcoin(): boolean {
|
||||||
const paymentMethod = this.estimate?.availablePaymentMethods?.bitcoin;
|
const paymentMethod = this.estimate?.availablePaymentMethods?.bitcoin;
|
||||||
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max;
|
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max;
|
||||||
}
|
}
|
||||||
|
|
||||||
get canPayWithCashapp() {
|
get canPayWithCashapp(): boolean {
|
||||||
if (!this.cashappEnabled || !this.conversions) {
|
if (!this.cashappEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -718,8 +845,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get canPayWithApplePay() {
|
get canPayWithApplePay(): boolean {
|
||||||
if (!this.applePayEnabled || !this.conversions) {
|
if (!this.applePayEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -734,7 +861,23 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get canPayWithBalance() {
|
get canPayWithGooglePay(): boolean {
|
||||||
|
if (!this.googlePayEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentMethod = this.estimate?.availablePaymentMethods?.googlePay;
|
||||||
|
if (paymentMethod) {
|
||||||
|
const costUSD = (this.cost / 100_000_000 * this.conversions.USD);
|
||||||
|
if (costUSD >= paymentMethod.min && costUSD <= paymentMethod.max) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canPayWithBalance(): boolean {
|
||||||
if (!this.hasAccessToBalanceMode) {
|
if (!this.hasAccessToBalanceMode) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -742,11 +885,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
|||||||
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max && this.cost <= this.estimate?.userBalance;
|
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max && this.cost <= this.estimate?.userBalance;
|
||||||
}
|
}
|
||||||
|
|
||||||
get canPay() {
|
get canPay(): boolean {
|
||||||
return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp || this.canPayWithApplePay;
|
return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp || this.canPayWithApplePay || this.canPayWithGooglePay;
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasAccessToBalanceMode() {
|
get hasAccessToBalanceMode(): boolean {
|
||||||
return this.isLoggedIn() && this.estimate?.hasAccess;
|
return this.isLoggedIn() && this.estimate?.hasAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
|
<span class="fee">{{ bar.class === 'tx' ? '' : '+' }}{{ bar.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></span>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -0,0 +1,63 @@
|
|||||||
|
<div
|
||||||
|
#tooltip
|
||||||
|
*ngIf="accelerationInfo && tooltipPosition !== null"
|
||||||
|
class="acceleration-tooltip"
|
||||||
|
[style.left]="tooltipPosition.x + 'px'"
|
||||||
|
[style.top]="tooltipPosition.y + 'px'"
|
||||||
|
>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="label" i18n="transaction.status|Transaction Status">Status</td>
|
||||||
|
<td class="value">
|
||||||
|
@if (accelerationInfo.status === 'seen') {
|
||||||
|
<span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span>
|
||||||
|
} @else if (accelerationInfo.status === 'accelerated') {
|
||||||
|
<span class="badge badge-accelerated" i18n="transaction.audit.accelerated">Accelerated</span>
|
||||||
|
} @else if (accelerationInfo.status === 'mined') {
|
||||||
|
<span class="badge badge-success" i18n="transaction.rbf.mined">Mined</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="accelerationInfo.fee">
|
||||||
|
<td class="label" i18n="transaction.fee|Transaction fee">Fee</td>
|
||||||
|
<td class="value">{{ accelerationInfo.fee | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="accelerationInfo.bidBoost >= 0 || accelerationInfo.feeDelta">
|
||||||
|
<td class="label" i18n="transaction.out-of-band-fees">Out-of-band fees</td>
|
||||||
|
@if (accelerationInfo.status === 'accelerated') {
|
||||||
|
<td class="value oobFees">{{ accelerationInfo.feeDelta | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
|
||||||
|
} @else {
|
||||||
|
<td class="value oobFees">{{ accelerationInfo.bidBoost | number }} <span class="symbol" i18n="shared.sats">sats</span></td>
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="accelerationInfo.fee && accelerationInfo.weight">
|
||||||
|
@if (accelerationInfo.status === 'seen') {
|
||||||
|
<td class="label" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</td>
|
||||||
|
<td class="value"><app-fee-rate [fee]="accelerationInfo.fee" [weight]="accelerationInfo.weight"></app-fee-rate></td>
|
||||||
|
} @else if (accelerationInfo.status === 'accelerated' || accelerationInfo.status === 'mined') {
|
||||||
|
<td class="label" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
|
||||||
|
@if (accelerationInfo.status === 'accelerated') {
|
||||||
|
<td class="value oobFees"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.feeDelta || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
|
||||||
|
} @else {
|
||||||
|
<td class="value oobFees"><app-fee-rate [fee]="accelerationInfo.fee + (accelerationInfo.bidBoost || 0)" [weight]="accelerationInfo.weight"></app-fee-rate></td>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="['accelerated', 'mined'].includes(accelerationInfo.status) && hasPoolsData()">
|
||||||
|
<td class="label" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
|
||||||
|
<td class="value" *ngIf="accelerationInfo.pools">
|
||||||
|
<ng-container *ngFor="let pool of accelerationInfo.pools; let i = index;">
|
||||||
|
<img *ngIf="accelerationInfo.poolsData[pool]"
|
||||||
|
class="pool-logo"
|
||||||
|
[style.opacity]="accelerationInfo?.minedByPoolUniqueId && pool !== accelerationInfo?.minedByPoolUniqueId ? '0.3' : '1'"
|
||||||
|
[src]="'/resources/mining-pools/' + accelerationInfo.poolsData[pool].slug + '.svg'"
|
||||||
|
onError="this.src = '/resources/mining-pools/default.svg'"
|
||||||
|
[alt]="'Logo of ' + pool.name + ' mining pool'">
|
||||||
|
<br *ngIf="i % 6 === 5">
|
||||||
|
</ng-container>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
.acceleration-tooltip {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 3;
|
||||||
|
background: color-mix(in srgb, var(--active-bg) 95%, transparent);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
|
||||||
|
color: var(--tooltip-grey);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 15px;
|
||||||
|
text-align: left;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.badge.badge-accelerated {
|
||||||
|
background-color: var(--tertiary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
padding-right: 30px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pool-logo {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oobFees {
|
||||||
|
color: #905cf4;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { Component, ElementRef, ViewChild, Input, OnChanges } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-acceleration-timeline-tooltip',
|
||||||
|
templateUrl: './acceleration-timeline-tooltip.component.html',
|
||||||
|
styleUrls: ['./acceleration-timeline-tooltip.component.scss'],
|
||||||
|
})
|
||||||
|
export class AccelerationTimelineTooltipComponent implements OnChanges {
|
||||||
|
@Input() accelerationInfo: any;
|
||||||
|
@Input() cursorPosition: { x: number, y: number };
|
||||||
|
|
||||||
|
tooltipPosition: any = null;
|
||||||
|
|
||||||
|
@ViewChild('tooltip') tooltipElement: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
ngOnChanges(changes): void {
|
||||||
|
if (changes.cursorPosition && changes.cursorPosition.currentValue) {
|
||||||
|
let x = Math.max(10, changes.cursorPosition.currentValue.x - 50);
|
||||||
|
let y = changes.cursorPosition.currentValue.y + 20;
|
||||||
|
if (this.tooltipElement) {
|
||||||
|
const elementBounds = this.tooltipElement.nativeElement.getBoundingClientRect();
|
||||||
|
if ((x + elementBounds.width) > (window.innerWidth - 10)) {
|
||||||
|
x = Math.max(0, window.innerWidth - elementBounds.width - 10);
|
||||||
|
}
|
||||||
|
if (y + elementBounds.height > (window.innerHeight - 20)) {
|
||||||
|
y = y - elementBounds.height - 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.tooltipPosition = { x, y };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPoolsData(): boolean {
|
||||||
|
return Object.keys(this.accelerationInfo.poolsData).length > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 animate"></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>
|
||||||
@ -38,18 +42,16 @@
|
|||||||
<div class="node-spacer"></div>
|
<div class="node-spacer"></div>
|
||||||
<div class="interval">
|
<div class="interval">
|
||||||
<div class="interval-time">
|
<div class="interval-time">
|
||||||
<app-time [time]="acceleratedAt - transactionTime"></app-time>
|
<app-time [time]="firstSeenToAccelerated"></app-time>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<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 (tx.status.confirmed) {
|
@if (tx.status.confirmed) {
|
||||||
<div class="interval-time">
|
<app-time [time]="acceleratedToMined"></app-time>
|
||||||
<app-time [time]="tx.status.block_time - acceleratedAt"></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>
|
||||||
@ -58,7 +60,7 @@
|
|||||||
<div class="nodes">
|
<div class="nodes">
|
||||||
<div class="node" [id]="'first-seen'">
|
<div class="node" [id]="'first-seen'">
|
||||||
<div class="seen-to-acc right"></div>
|
<div class="seen-to-acc right"></div>
|
||||||
<div class="shape-border">
|
<div class="shape-border hovering" (pointerover)="onHover($event, 'seen');" (pointerout)="onBlur($event);">
|
||||||
<div class="shape"></div>
|
<div class="shape"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
|
<div class="status"><span class="badge badge-primary" i18n="transaction.first-seen|Transaction first seen">First seen</span></div>
|
||||||
@ -73,47 +75,50 @@
|
|||||||
<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">
|
<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 offset-left" [class.no-margin]="!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]="true"></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>
|
||||||
}
|
}
|
||||||
<div class="shape-border" [class.waiting]="!tx.status.confirmed">
|
<div class="shape-border"
|
||||||
|
[ngClass]="{'waiting': !tx.status.confirmed, 'hovering': tx.status.confirmed}"
|
||||||
|
(pointerover)="onHover($event, tx.status.confirmed ? 'mined' : null)"
|
||||||
|
(pointerout)="onBlur($event);">
|
||||||
<div class="shape"></div>
|
<div class="shape"></div>
|
||||||
</div>
|
</div>
|
||||||
@if (tx.status.confirmed) {
|
@if (tx.status.confirmed) {
|
||||||
@ -130,4 +135,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<app-acceleration-timeline-tooltip
|
||||||
|
[accelerationInfo]="hoverInfo"
|
||||||
|
[cursorPosition]="tooltipPosition"
|
||||||
|
></app-acceleration-timeline-tooltip>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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 {
|
||||||
@ -152,9 +155,16 @@
|
|||||||
margin-bottom: -8px;
|
margin-bottom: -8px;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
transition: background-color 300ms, padding 300ms;
|
||||||
|
|
||||||
|
&.hovering {
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.shape {
|
.shape {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { Component, Input, OnInit, OnChanges } 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 { MiningService } from '@app/services/mining.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-acceleration-timeline',
|
selector: 'app-acceleration-timeline',
|
||||||
@ -9,47 +11,82 @@ import { Transaction } from '../../interfaces/electrs.interface';
|
|||||||
})
|
})
|
||||||
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() 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;
|
||||||
|
acceleratedToMined: number;
|
||||||
|
|
||||||
constructor() {}
|
tooltipPosition = null;
|
||||||
|
hoverInfo: any = null;
|
||||||
|
poolsData: { [id: number]: SinglePoolStats } = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private miningService: MiningService,
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
|
this.updateTimes();
|
||||||
this.now = Math.floor(new Date().getTime() / 1000);
|
|
||||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
|
||||||
|
|
||||||
this.interval = window.setInterval(() => {
|
this.miningService.getPools().subscribe(pools => {
|
||||||
this.now = Math.floor(new Date().getTime() / 1000);
|
for (const pool of pools) {
|
||||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
this.poolsData[pool.unique_id] = pool;
|
||||||
}, 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));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
updateTimes(): void {
|
||||||
clearInterval(this.interval);
|
this.now = Math.floor(new Date().getTime() / 1000);
|
||||||
|
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
||||||
|
this.firstSeenToAccelerated = Math.max(0, this.acceleratedAt - this.transactionTime);
|
||||||
|
this.acceleratedToMined = Math.max(0, this.tx.status.block_time - this.acceleratedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
onHover(event, status: string): void {
|
||||||
|
if (status === 'seen') {
|
||||||
|
this.hoverInfo = {
|
||||||
|
status,
|
||||||
|
fee: this.tx.fee,
|
||||||
|
weight: this.tx.weight
|
||||||
|
};
|
||||||
|
} else if (status === 'accelerated') {
|
||||||
|
this.hoverInfo = {
|
||||||
|
status,
|
||||||
|
fee: this.accelerationInfo?.effectiveFee || this.tx.fee,
|
||||||
|
weight: this.tx.weight,
|
||||||
|
feeDelta: this.accelerationInfo?.feeDelta || this.tx.feeDelta,
|
||||||
|
pools: this.tx.acceleratedBy || this.accelerationInfo?.pools,
|
||||||
|
poolsData: this.poolsData
|
||||||
|
};
|
||||||
|
} else if (status === 'mined') {
|
||||||
|
this.hoverInfo = {
|
||||||
|
status,
|
||||||
|
fee: this.accelerationInfo?.effectiveFee,
|
||||||
|
weight: this.tx.weight,
|
||||||
|
bidBoost: this.accelerationInfo?.bidBoost,
|
||||||
|
minedByPoolUniqueId: this.accelerationInfo?.minedByPoolUniqueId,
|
||||||
|
pools: this.tx.acceleratedBy || this.accelerationInfo?.pools,
|
||||||
|
poolsData: this.poolsData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlur(event): void {
|
||||||
|
this.hoverInfo = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('pointermove', ['$event'])
|
||||||
|
onPointerMove(event) {
|
||||||
|
this.tooltipPosition = { x: event.clientX, y: event.clientY };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
@ -23,7 +23,7 @@ import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: calc(50% - 15px);
|
left: calc(50% - 15px);
|
||||||
z-index: 100;
|
z-index: 99;
|
||||||
}
|
}
|
||||||
`],
|
`],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@ -264,7 +264,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
|
|||||||
type: 'bar',
|
type: 'bar',
|
||||||
barWidth: '90%',
|
barWidth: '90%',
|
||||||
large: true,
|
large: true,
|
||||||
barMinHeight: 1,
|
barMinHeight: 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
dataZoom: (this.widget || data.length === 0 )? undefined : [{
|
dataZoom: (this.widget || data.length === 0 )? undefined : [{
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
<div class="acceleration-list" *ngIf="accelerationList$ | async as accelerations">
|
<div class="acceleration-list" *ngIf="{ accelerations: accelerationList$ | async } as state">
|
||||||
<table *ngIf="!accelerations || accelerations.length; 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>
|
||||||
<ng-container *ngIf="pending">
|
<ng-container *ngIf="pending">
|
||||||
@ -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="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>
|
||||||
@ -33,7 +33,7 @@
|
|||||||
<app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate>
|
<app-fee-rate [fee]="acceleration.effectiveFee" [weight]="acceleration.effectiveVsize * 4"></app-fee-rate>
|
||||||
</td>
|
</td>
|
||||||
<td class="bid text-right">
|
<td class="bid text-right">
|
||||||
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
{{ (acceleration.feeDelta) | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="time text-right">
|
<td class="time text-right">
|
||||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
||||||
@ -41,7 +41,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="!pending">
|
<ng-container *ngIf="!pending">
|
||||||
<td *ngIf="acceleration.boost != null" class="fee text-right">
|
<td *ngIf="acceleration.boost != null" class="fee text-right">
|
||||||
{{ acceleration.boost | number }} <span class="symbol" i18n="shared.sat|sat">sat</span>
|
{{ acceleration.boost | number }} <span class="symbol" i18n="shared.sats">sats</span>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="acceleration.boost == null" class="fee text-right">
|
<td *ngIf="acceleration.boost == null" class="fee text-right">
|
||||||
~
|
~
|
||||||
@ -62,8 +62,9 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="status text-right">
|
<td class="status text-right">
|
||||||
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
|
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
|
||||||
<span *ngIf="acceleration.status.includes('completed')" class="badge badge-success" i18n="">Completed <span *ngIf="acceleration.status === 'completed_provisional'">🔄</span></span>
|
<span *ngIf="acceleration.status.includes('completed') && acceleration.minedByPoolUniqueId && pools[acceleration.minedByPoolUniqueId]" class="badge badge-success"><ng-container i18n="accelerator.completed">Completed</ng-container><span *ngIf="acceleration.status === 'completed_provisional'"> ⌛</span></span>
|
||||||
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger" i18n="accelerator.canceled">Failed <span *ngIf="acceleration.status === 'failed_provisional'">🔄</span></span>
|
<span *ngIf="acceleration.status.includes('completed') && (!acceleration.minedByPoolUniqueId || !pools[acceleration.minedByPoolUniqueId])" class="badge badge-success"><ng-container i18n="transaction.rbf.mined">Mined</ng-container><span *ngIf="acceleration.status === 'completed_provisional'"> ⌛</span></span>
|
||||||
|
<span *ngIf="acceleration.status.includes('failed')" class="badge badge-danger"><ng-container i18n="accelerator.canceled">Canceled</ng-container><span *ngIf="acceleration.status === 'failed_provisional'"> ⌛</span></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="date text-right" *ngIf="!this.widget">
|
<td class="date text-right" *ngIf="!this.widget">
|
||||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
||||||
@ -72,22 +73,47 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
<ng-template #skeleton>
|
<ng-template #skeleton>
|
||||||
<tbody>
|
@if (!pending) {
|
||||||
<tr *ngFor="let item of skeletonLines">
|
<tbody>
|
||||||
<td class="txid text-left">
|
<tr *ngFor="let item of skeletonLines">
|
||||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
<td class="txid text-left">
|
||||||
</td>
|
<span class="skeleton-loader" style="max-width: 200px"></span>
|
||||||
<td class="fee text-right">
|
</td>
|
||||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
<td class="fee text-right">
|
||||||
</td>
|
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||||
<td class="fee-delta text-right">
|
</td>
|
||||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
<td class="block text-right">
|
||||||
</td>
|
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||||
<td class="status text-right">
|
</td>
|
||||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
<td class="pool text-right" *ngIf="!this.widget">
|
||||||
</td>
|
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||||
</tr>
|
</td>
|
||||||
</tbody>
|
<td class="status text-right">
|
||||||
|
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||||
|
</td>
|
||||||
|
<td class="date text-right" *ngIf="!this.widget">
|
||||||
|
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
} @else {
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let item of skeletonLines">
|
||||||
|
<td class="txid text-left">
|
||||||
|
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||||
|
</td>
|
||||||
|
<td class="fee-rate text-right">
|
||||||
|
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||||
|
</td>
|
||||||
|
<td class="bid text-right">
|
||||||
|
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||||
|
</td>
|
||||||
|
<td class="time text-right">
|
||||||
|
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|||||||
@ -85,8 +85,8 @@ tr, td, th {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pool-logo {
|
.pool-logo {
|
||||||
width: 22px;
|
width: 18px;
|
||||||
height: 22px;
|
height: 18px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
|
|||||||
@ -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, filter, of, switchMap, tap, throttleTime } 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',
|
||||||
@ -32,6 +32,7 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
|||||||
dir: 'rtl' | 'ltr' = 'ltr';
|
dir: 'rtl' | 'ltr' = 'ltr';
|
||||||
paramSubscription: Subscription;
|
paramSubscription: Subscription;
|
||||||
pools: { [id: number]: SinglePoolStats } = {};
|
pools: { [id: number]: SinglePoolStats } = {};
|
||||||
|
nonEmptyAccelerations: boolean = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private servicesApiService: ServicesApiServices,
|
private servicesApiService: ServicesApiServices,
|
||||||
@ -50,12 +51,21 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.miningService.getPools().subscribe(pools => {
|
||||||
|
for (const pool of pools) {
|
||||||
|
this.pools[pool.unique_id] = pool;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (!this.widget) {
|
if (!this.widget) {
|
||||||
this.websocketService.want(['blocks']);
|
this.websocketService.want(['blocks']);
|
||||||
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
|
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
|
||||||
|
|
||||||
this.paramSubscription = this.route.params.pipe(
|
this.paramSubscription = combineLatest([
|
||||||
tap(params => {
|
this.route.params,
|
||||||
|
timer(0),
|
||||||
|
]).pipe(
|
||||||
|
tap(([params]) => {
|
||||||
this.page = +params['page'] || 1;
|
this.page = +params['page'] || 1;
|
||||||
this.pageSubject.next(this.page);
|
this.pageSubject.next(this.page);
|
||||||
})
|
})
|
||||||
@ -82,12 +92,6 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
|||||||
).subscribe(() => {
|
).subscribe(() => {
|
||||||
this.pageChange(this.page);
|
this.pageChange(this.page);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.miningService.getMiningStats('1m').subscribe(stats => {
|
|
||||||
for (const pool of stats.pools) {
|
|
||||||
this.pools[pool.poolUniqueId] = pool;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
|
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
|
||||||
@ -115,6 +119,7 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
|||||||
for (const acc of accelerations) {
|
for (const acc of accelerations) {
|
||||||
acc.boost = acc.boostCost != null ? acc.boostCost : acc.bidBoost;
|
acc.boost = acc.boostCost != null ? acc.boostCost : acc.bidBoost;
|
||||||
}
|
}
|
||||||
|
this.nonEmptyAccelerations = accelerations.length > 0;
|
||||||
if (this.widget) {
|
if (this.widget) {
|
||||||
return of(accelerations.slice(0, 6));
|
return of(accelerations.slice(0, 6));
|
||||||
} else {
|
} else {
|
||||||
@ -146,4 +151,4 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
|||||||
this.paramSubscription?.unsubscribe();
|
this.paramSubscription?.unsubscribe();
|
||||||
this.keyNavigationSubscription?.unsubscribe();
|
this.keyNavigationSubscription?.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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'));
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
@if (chartOnly) {
|
@if (chartOnly) {
|
||||||
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
||||||
} @else {
|
} @else {
|
||||||
<table>
|
<table style="width: 100%;">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="td-width field-label" [class]="chartPositionLeft ? 'chart-left' : ''" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td>
|
<td class="td-width field-label" [class]="chartPositionLeft ? 'chart-left' : ''" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td>
|
||||||
@ -10,17 +10,17 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
|
<td class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
|
||||||
<div class="effective-fee-container">
|
<div class="effective-fee-container">
|
||||||
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
|
@if (accelerationInfo?.acceleratedFeeRate && (!effectiveFeeRate || accelerationInfo.acceleratedFeeRate >= effectiveFeeRate)) {
|
||||||
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
<app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||||
} @else {
|
} @else {
|
||||||
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
<app-fee-rate class="oobFees" [fee]="effectiveFeeRate"></app-fee-rate>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<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>
|
||||||
|
|||||||
@ -61,4 +61,8 @@
|
|||||||
& > div, & > div > svg {
|
& > div, & > div > svg {
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.oobFees {
|
||||||
|
color: #905cf4;
|
||||||
}
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter } 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 {
|
||||||
@ -23,7 +23,8 @@ function toRGB({r,g,b}): string {
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ActiveAccelerationBox implements OnChanges {
|
export class ActiveAccelerationBox implements OnChanges {
|
||||||
@Input() tx: Transaction;
|
@Input() acceleratedBy?: number[];
|
||||||
|
@Input() effectiveFeeRate?: number;
|
||||||
@Input() accelerationInfo: Acceleration;
|
@Input() accelerationInfo: Acceleration;
|
||||||
@Input() miningStats: MiningStats;
|
@Input() miningStats: MiningStats;
|
||||||
@Input() pools: number[];
|
@Input() pools: number[];
|
||||||
@ -41,10 +42,12 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||||||
timespan = '';
|
timespan = '';
|
||||||
chartInstance: any = undefined;
|
chartInstance: any = undefined;
|
||||||
|
|
||||||
constructor() {}
|
constructor(
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
const pools = this.pools || this.accelerationInfo?.pools || this.tx.acceleratedBy;
|
const pools = this.pools || this.accelerationInfo?.pools || this.acceleratedBy;
|
||||||
if (pools && this.miningStats) {
|
if (pools && this.miningStats) {
|
||||||
this.prepareChartOptions(pools);
|
this.prepareChartOptions(pools);
|
||||||
}
|
}
|
||||||
@ -67,16 +70,27 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||||||
|
|
||||||
const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate);
|
const acceleratingPools = (poolList || []).filter(id => pools[id]).sort((a,b) => pools[a].lastEstimatedHashrate - pools[b].lastEstimatedHashrate);
|
||||||
const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
|
const totalAcceleratedHashrate = acceleratingPools.reduce((total, pool) => total + pools[pool].lastEstimatedHashrate, 0);
|
||||||
|
// Find the first pool with at least 1% of the total network hashrate
|
||||||
|
const firstSignificantPool = acceleratingPools.findIndex(pool => pools[pool].lastEstimatedHashrate > this.miningStats.lastEstimatedHashrate / 100);
|
||||||
|
const numSignificantPools = acceleratingPools.length - firstSignificantPool;
|
||||||
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,
|
||||||
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * .08)),
|
color,
|
||||||
`<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(
|
||||||
@ -127,6 +141,7 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
onChartInit(ec) {
|
onChartInit(ec) {
|
||||||
@ -139,4 +154,4 @@ export class ActiveAccelerationBox implements OnChanges {
|
|||||||
onToggleCpfp(): void {
|
onToggleCpfp(): void {
|
||||||
this.toggleCpfp.emit();
|
this.toggleCpfp.emit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
<div class="sparkles" #sparkleAnchor>
|
||||||
|
<div *ngFor="let sparkle of sparkles" class="sparkle" [style]="sparkle.style">
|
||||||
|
<span class="inner-sparkle" [style]="sparkle.rotation">+</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
.sparkles {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--block-size);
|
||||||
|
height: 50px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sparkle {
|
||||||
|
position: absolute;
|
||||||
|
color: rgba(152, 88, 255, 0.75);
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8) rotate(0deg);
|
||||||
|
animation: pop ease 2000ms forwards, sparkle ease 500ms infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-sparkle {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pop {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.8) rotate(0deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: scale(1) rotate(72deg);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0) rotate(360deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sparkle {
|
||||||
|
0% {
|
||||||
|
color: rgba(152, 88, 255, 0.75);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
color: rgba(198, 162, 255, 0.75);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
color: rgba(152, 88, 255, 0.75);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-acceleration-sparkles',
|
||||||
|
templateUrl: './acceleration-sparkles.component.html',
|
||||||
|
styleUrls: ['./acceleration-sparkles.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class AccelerationSparklesComponent implements OnChanges {
|
||||||
|
@Input() arrow: ElementRef<HTMLDivElement>;
|
||||||
|
@Input() run: boolean = false;
|
||||||
|
|
||||||
|
@ViewChild('sparkleAnchor')
|
||||||
|
sparkleAnchor: ElementRef<HTMLDivElement>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
endTimeout: any;
|
||||||
|
lastSparkle: number = 0;
|
||||||
|
sparkleWidth: number = 0;
|
||||||
|
sparkles: any[] = [];
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (changes.run) {
|
||||||
|
if (this.endTimeout) {
|
||||||
|
clearTimeout(this.endTimeout);
|
||||||
|
this.endTimeout = null;
|
||||||
|
}
|
||||||
|
if (this.run) {
|
||||||
|
this.doSparkle();
|
||||||
|
} else {
|
||||||
|
this.endTimeout = setTimeout(() => {
|
||||||
|
this.sparkles = [];
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doSparkle(): void {
|
||||||
|
if (this.run) {
|
||||||
|
const now = performance.now();
|
||||||
|
if (now - this.lastSparkle > 20) {
|
||||||
|
this.lastSparkle = now;
|
||||||
|
if (this.arrow?.nativeElement && this.sparkleAnchor?.nativeElement) {
|
||||||
|
const anchor = this.sparkleAnchor.nativeElement.getBoundingClientRect().right;
|
||||||
|
const right = this.arrow.nativeElement.getBoundingClientRect().right;
|
||||||
|
const dx = (anchor - right) + 30;
|
||||||
|
const numSparkles = Math.ceil(Math.random() * 3);
|
||||||
|
for (let i = 0; i < numSparkles; i++) {
|
||||||
|
this.sparkles.push({
|
||||||
|
style: {
|
||||||
|
right: (dx + (Math.random() * 10)) + 'px',
|
||||||
|
top: (15 + (Math.random() * 30)) + 'px',
|
||||||
|
},
|
||||||
|
rotation: {
|
||||||
|
transform: `rotate(${Math.random() * 360}deg)`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
while (this.sparkles.length > 200) {
|
||||||
|
this.sparkles.shift();
|
||||||
|
}
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.doSparkle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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),
|
||||||
@ -30,7 +29,7 @@ const periodSeconds = {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: calc(50% - 15px);
|
left: calc(50% - 15px);
|
||||||
z-index: 100;
|
z-index: 99;
|
||||||
}
|
}
|
||||||
`],
|
`],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
@ -45,6 +44,7 @@ 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;
|
||||||
|
|
||||||
data: any[] = [];
|
data: any[] = [];
|
||||||
fiatData: any[] = [];
|
fiatData: any[] = [];
|
||||||
@ -77,7 +77,6 @@ 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,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -86,6 +85,9 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
if (!this.addressSummary$ && (!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();
|
||||||
@ -147,7 +149,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
if (!summary) {
|
if (!summary) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0);
|
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;
|
let runningTotal = total;
|
||||||
const processData = summary.map(d => {
|
const processData = summary.map(d => {
|
||||||
@ -161,7 +163,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
d
|
d
|
||||||
};
|
};
|
||||||
}).reverse();
|
}).reverse();
|
||||||
|
|
||||||
this.data = processData.filter(({ d }) => d.txid !== undefined).map(({ time, balance, d }) => [time, balance, d]);
|
this.data = processData.filter(({ d }) => d.txid !== undefined).map(({ time, balance, d }) => [time, balance, d]);
|
||||||
this.fiatData = processData.map(({ time, fiatBalance, balance, d }) => [time, fiatBalance, d, balance]);
|
this.fiatData = processData.map(({ time, fiatBalance, balance, d }) => [time, fiatBalance, d, balance]);
|
||||||
|
|
||||||
@ -179,6 +181,9 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
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.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight;
|
||||||
|
this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40;
|
||||||
|
|
||||||
this.chartOptions = {
|
this.chartOptions = {
|
||||||
color: [
|
color: [
|
||||||
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
@ -245,21 +250,22 @@ 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');
|
||||||
|
|
||||||
const btcVal = btcData.reduce((total, d) => total + d.data[2].value, 0);
|
const btcVal = btcData.reduce((total, d) => total + d.data[2].value, 0);
|
||||||
const fiatVal = fiatData.reduce((total, d) => total + d.data[2].value * d.data[2].price / 100_000_000, 0);
|
const fiatVal = fiatData.reduce((total, d) => total + d.data[2].value * d.data[2].price / 100_000_000, 0);
|
||||||
const btcColor = btcVal === 0 ? '' : (btcVal > 0 ? 'var(--green)' : 'var(--red)');
|
const btcColor = btcVal === 0 ? '' : (btcVal > 0 ? 'var(--green)' : 'var(--red)');
|
||||||
@ -291,7 +297,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip += `</div><span>${date}</span></div>`;
|
tooltip += `</div></div>`;
|
||||||
return tooltip;
|
return tooltip;
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
},
|
},
|
||||||
@ -311,18 +317,21 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
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`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -336,7 +345,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
axisLabel: {
|
axisLabel: {
|
||||||
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, 0, undefined, true)}`;
|
||||||
}.bind(this)
|
}.bind(this)
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
@ -440,7 +449,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
|||||||
right: this.right,
|
right: this.right,
|
||||||
}] : undefined
|
}] : undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.chartInstance) {
|
if (this.chartInstance) {
|
||||||
this.chartInstance.setOption(this.chartOptions);
|
this.chartInstance.setOption(this.chartOptions);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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',
|
||||||
@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleVin() {
|
handleVin() {
|
||||||
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin])
|
const address = new AddressTypeInfo(this.network || 'mainnet', this.vin.prevout?.scriptpubkey_address, this.vin.prevout?.scriptpubkey_type as AddressType, [this.vin]);
|
||||||
if (address?.scripts.size) {
|
if (address?.scripts.size) {
|
||||||
const script = address?.scripts.values().next().value;
|
const script = address?.scripts.values().next().value;
|
||||||
if (script.template?.label) {
|
if (script.template?.label) {
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user