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®
|
||||
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
|
||||
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®,
|
||||
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 registered trademarks or trademarks
|
||||
of Mempool Space K.K in Japan, the United States, and/or other countries.
|
||||
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
|
||||
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
|
||||
<https://mempool.space/trademark-policy>.
|
||||
|
@ -77,7 +77,7 @@ Query OK, 0 rows affected (0.00 sec)
|
||||
|
||||
#### 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._
|
||||
|
||||
|
@ -27,8 +27,9 @@
|
||||
"AUTOMATIC_POOLS_UPDATE": false,
|
||||
"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_UPDATE_DELAY": 604800,
|
||||
"AUDIT": false,
|
||||
"RUST_GBT": false,
|
||||
"RUST_GBT": true,
|
||||
"LIMIT_GBT": false,
|
||||
"CPFP_INDEXING": false,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||
@ -45,7 +46,8 @@
|
||||
"PASSWORD": "mempool",
|
||||
"TIMEOUT": 60000,
|
||||
"COOKIE": false,
|
||||
"COOKIE_PATH": "/path/to/bitcoin/.cookie"
|
||||
"COOKIE_PATH": "/path/to/bitcoin/.cookie",
|
||||
"DEBUG_LOG_PATH": "/path/to/bitcoin/debug.log"
|
||||
},
|
||||
"ELECTRUM": {
|
||||
"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",
|
||||
"version": "3.0.0-dev",
|
||||
"version": "3.1.0-dev",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
@ -39,24 +39,24 @@
|
||||
"prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@mempool/electrum-client": "1.1.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"axios": "~1.7.2",
|
||||
"axios": "1.7.2",
|
||||
"bitcoinjs-lib": "~6.1.3",
|
||||
"crypto-js": "~4.2.0",
|
||||
"express": "~4.19.2",
|
||||
"express": "~4.21.1",
|
||||
"maxmind": "~4.3.11",
|
||||
"mysql2": "~3.10.0",
|
||||
"mysql2": "~3.11.0",
|
||||
"rust-gbt": "file:./rust-gbt",
|
||||
"redis": "^4.6.6",
|
||||
"redis": "^4.7.0",
|
||||
"socks-proxy-agent": "~7.0.0",
|
||||
"typescript": "~4.9.3",
|
||||
"ws": "~8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/core": "^7.25.2",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/express": "^4.17.17",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"INDEXING_BLOCKS_AMOUNT": 14,
|
||||
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
||||
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
|
||||
"POOLS_UPDATE_DELAY": 604800,
|
||||
"AUDIT": true,
|
||||
"RUST_GBT": false,
|
||||
"LIMIT_GBT": false,
|
||||
@ -46,7 +47,8 @@
|
||||
"PASSWORD": "__CORE_RPC_PASSWORD__",
|
||||
"TIMEOUT": 1000,
|
||||
"COOKIE": false,
|
||||
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__"
|
||||
"COOKIE_PATH": "__CORE_RPC_COOKIE_PATH__",
|
||||
"DEBUG_LOG_PATH": "__CORE_RPC_DEBUG_LOG_PATH__"
|
||||
},
|
||||
"ELECTRUM": {
|
||||
"HOST": "__ELECTRUM_HOST__",
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 replacedTransactions = require('./test-data/transactions-replaced.json');
|
||||
@ -10,14 +10,14 @@ describe('Common', () => {
|
||||
describe('RBF', () => {
|
||||
const newTransactions = rbfTransactions.concat(randomTransactions);
|
||||
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(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||
});
|
||||
|
||||
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(result).toHaveProperty('7219d95161f3718335991ac6d967d24eedec370908c9879bb1e192e6d797d0a6');
|
||||
expect(result).toHaveProperty('5387881d695d4564d397026dc5f740f816f8390b4b2c5ec8c20309122712a875');
|
||||
|
@ -41,8 +41,9 @@ describe('Mempool Backend Config', () => {
|
||||
STDOUT_LOG_MIN_PRIORITY: 'debug',
|
||||
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_UPDATE_DELAY: 604800,
|
||||
AUDIT: false,
|
||||
RUST_GBT: false,
|
||||
RUST_GBT: true,
|
||||
LIMIT_GBT: false,
|
||||
CPFP_INDEXING: false,
|
||||
MAX_BLOCKS_BULK_QUERY: 0,
|
||||
@ -73,7 +74,8 @@ describe('Mempool Backend Config', () => {
|
||||
PASSWORD: 'mempool',
|
||||
TIMEOUT: 60000,
|
||||
COOKIE: false,
|
||||
COOKIE_PATH: '/bitcoin/.cookie'
|
||||
COOKIE_PATH: '/bitcoin/.cookie',
|
||||
DEBUG_LOG_PATH: '',
|
||||
});
|
||||
|
||||
expect(config.SECOND_CORE_RPC).toStrictEqual({
|
||||
|
@ -70,7 +70,7 @@ class AboutRoutes {
|
||||
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/', '')}`;
|
||||
try {
|
||||
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
|
||||
|
@ -2,24 +2,28 @@ import config from '../config';
|
||||
import logger from '../logger';
|
||||
import { MempoolTransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||
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
|
||||
|
||||
class Audit {
|
||||
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
|
||||
: { censored: string[], added: string[], prioritized: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
|
||||
auditBlock(height: number, transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended })
|
||||
: { 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) {
|
||||
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 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 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 isCensored = {}; // missing, without excuse
|
||||
const isDisplaced = {};
|
||||
const isAccelerated = {};
|
||||
let displacedWeight = 0;
|
||||
let matchedWeight = 0;
|
||||
let projectedWeight = 0;
|
||||
@ -32,6 +36,7 @@ class Audit {
|
||||
inBlock[tx.txid] = tx;
|
||||
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
|
||||
accelerated.push(tx.txid);
|
||||
isAccelerated[tx.txid] = true;
|
||||
}
|
||||
}
|
||||
// coinbase is always expected
|
||||
@ -113,11 +118,16 @@ class Audit {
|
||||
} else {
|
||||
if (rbfCache.has(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]) {
|
||||
prioritized.push(tx.txid);
|
||||
if (isDisplaced[tx.txid]) {
|
||||
added.push(tx.txid);
|
||||
}
|
||||
} else {
|
||||
added.push(tx.txid);
|
||||
unseen.push(tx.txid);
|
||||
}
|
||||
}
|
||||
overflowWeight += tx.weight;
|
||||
@ -125,6 +135,8 @@ class Audit {
|
||||
totalWeight += tx.weight;
|
||||
}
|
||||
|
||||
({ prioritized, deprioritized } = transactionUtils.identifyPrioritizedTransactions(transactions, 'effectiveFeePerVsize'));
|
||||
|
||||
// transactions missing from near the end of our template are probably not being censored
|
||||
let overflowWeightRemaining = overflowWeight - (config.MEMPOOL.BLOCK_WEIGHT_UNITS - totalWeight);
|
||||
let maxOverflowRate = 0;
|
||||
@ -165,6 +177,7 @@ class Audit {
|
||||
const similarity = projectedWeight ? matchedWeight / projectedWeight : 1;
|
||||
|
||||
return {
|
||||
unseen,
|
||||
censored: Object.keys(isCensored),
|
||||
added,
|
||||
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';
|
||||
|
||||
export interface AbstractBitcoinApi {
|
||||
@ -23,6 +23,7 @@ export interface AbstractBitcoinApi {
|
||||
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
||||
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
|
||||
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult>;
|
||||
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
|
||||
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
|
||||
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
|
||||
|
@ -218,3 +218,21 @@ export interface TestMempoolAcceptResult {
|
||||
},
|
||||
['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 { 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 blocks from '../blocks';
|
||||
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> {
|
||||
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
|
||||
return {
|
||||
@ -327,6 +331,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
||||
'witness_v1_taproot': 'v1_p2tr',
|
||||
'nonstandard': 'nonstandard',
|
||||
'multisig': 'multisig',
|
||||
'anchor': 'anchor',
|
||||
'nulldata': 'op_return'
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Application, NextFunction, Request, Response } from 'express';
|
||||
import logger from '../../logger';
|
||||
import bitcoinClient from './bitcoin-client';
|
||||
import config from '../../config';
|
||||
|
||||
/**
|
||||
* Define a set of routes used by the accelerator server
|
||||
@ -11,15 +12,15 @@ class BitcoinBackendRoutes {
|
||||
|
||||
public initRoutes(app: Application) {
|
||||
app
|
||||
.get('/api/internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry)
|
||||
.post('/api/internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
|
||||
.get('/api/internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
|
||||
.post('/api/internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
|
||||
.post('/api/internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
|
||||
.get('/api/internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
|
||||
.get('/api/internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
|
||||
.get('/api/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-mempool-entry', this.disableCache, this.$getMempoolEntry)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction)
|
||||
.post(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash)
|
||||
.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 rbfCache from '../rbf-cache';
|
||||
import { calculateMempoolTxCpfp } from '../cpfp';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class BitcoinRoutes {
|
||||
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 + 'block/:hash', this.getBlock)
|
||||
.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/tx/:txid/audit', this.$getBlockTxAuditSummary)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
|
||||
.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/: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') {
|
||||
@ -86,7 +90,7 @@ class BitcoinRoutes {
|
||||
res.set('Content-Type', 'application/json');
|
||||
res.send(result);
|
||||
} 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();
|
||||
res.json(result);
|
||||
} 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) {
|
||||
if (!Array.isArray(req.query.txId)) {
|
||||
res.status(500).send('Not an array');
|
||||
handleError(req, res, 500, 'Not an array');
|
||||
return;
|
||||
}
|
||||
const txIds: string[] = [];
|
||||
@ -128,12 +132,12 @@ class BitcoinRoutes {
|
||||
private async $getBatchedOutspends(req: Request, res: Response): Promise<IEsploraApi.Outspend[][] | void> {
|
||||
const txids_csv = req.query.txids;
|
||||
if (!txids_csv || typeof txids_csv !== 'string') {
|
||||
res.status(500).send('Invalid txids format');
|
||||
handleError(req, res, 500, 'Invalid txids format');
|
||||
return;
|
||||
}
|
||||
const txids = txids_csv.split(',');
|
||||
if (txids.length > 50) {
|
||||
res.status(400).send('Too many txids requested');
|
||||
handleError(req, res, 400, 'Too many txids requested');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -141,13 +145,13 @@ class BitcoinRoutes {
|
||||
const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txids);
|
||||
res.json(batchedOutspends);
|
||||
} 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -165,6 +169,7 @@ class BitcoinRoutes {
|
||||
acceleration: tx.acceleration,
|
||||
acceleratedBy: tx.acceleratedBy || undefined,
|
||||
acceleratedAt: tx.acceleratedAt || undefined,
|
||||
feeDelta: tx.feeDelta || undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -179,7 +184,7 @@ class BitcoinRoutes {
|
||||
try {
|
||||
cpfpInfo = await transactionRepository.$getCpfpInfo(req.params.txId);
|
||||
} catch (e) {
|
||||
res.status(500).send('failed to get CPFP info');
|
||||
handleError(req, res, 500, 'failed to get CPFP info');
|
||||
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) {
|
||||
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) {
|
||||
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
|
||||
// 422 Unprocessable Entity
|
||||
// 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) {
|
||||
if (e instanceof Error && new RegExp(notFoundError).test(e.message)) {
|
||||
res.status(404).send(e.message);
|
||||
handleError(req, res, 404, e.message);
|
||||
} 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) {
|
||||
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);
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24 * 30).toUTCString());
|
||||
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) {
|
||||
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.json(block);
|
||||
} 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.send(blockHeader);
|
||||
} 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.json(auditSummary);
|
||||
} else {
|
||||
return res.status(404).send(`audit not available`);
|
||||
handleError(req, res, 404, `audit not available`);
|
||||
return;
|
||||
}
|
||||
} 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.json(auditSummary);
|
||||
} else {
|
||||
return res.status(404).send(`transaction audit not available`);
|
||||
handleError(req, res, 404, `transaction audit not available`);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
@ -387,42 +408,49 @@ class BitcoinRoutes {
|
||||
return await this.getLegacyBlocks(req, res);
|
||||
}
|
||||
} 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) {
|
||||
try {
|
||||
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) {
|
||||
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()) {
|
||||
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);
|
||||
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);
|
||||
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) {
|
||||
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) {
|
||||
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.json(await blocks.$getBlocksBetweenHeight(from, to));
|
||||
|
||||
} 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.json(returnBlocks);
|
||||
} 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) {
|
||||
try {
|
||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, 0);
|
||||
@ -482,7 +510,7 @@ class BitcoinRoutes {
|
||||
res.json(transactions);
|
||||
} catch (e) {
|
||||
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));
|
||||
res.send(blockHash);
|
||||
} 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -506,15 +534,16 @@ class BitcoinRoutes {
|
||||
res.json(addressData);
|
||||
} catch (e) {
|
||||
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> {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -527,23 +556,23 @@ class BitcoinRoutes {
|
||||
res.json(transactions);
|
||||
} catch (e) {
|
||||
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;
|
||||
}
|
||||
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> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private async getScriptHash(req: Request, res: Response) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -554,15 +583,16 @@ class BitcoinRoutes {
|
||||
res.json(addressData);
|
||||
} catch (e) {
|
||||
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> {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -577,16 +607,16 @@ class BitcoinRoutes {
|
||||
res.json(transactions);
|
||||
} catch (e) {
|
||||
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;
|
||||
}
|
||||
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> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -596,7 +626,7 @@ class BitcoinRoutes {
|
||||
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
||||
res.send(blockHash);
|
||||
} 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();
|
||||
res.send(rawMempool);
|
||||
} 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 {
|
||||
const result = blocks.getCurrentBlockHeight();
|
||||
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.send(result.toString());
|
||||
} 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.send(result);
|
||||
} 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.send(result);
|
||||
} 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);
|
||||
res.json(result);
|
||||
} 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);
|
||||
res.json(result);
|
||||
} 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
|
||||
});
|
||||
} 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);
|
||||
res.json(result);
|
||||
} 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);
|
||||
res.json(result);
|
||||
} 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();
|
||||
}
|
||||
} 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);
|
||||
res.json(result);
|
||||
} 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) {
|
||||
res.json(da);
|
||||
} else {
|
||||
res.status(503).send(`Service Temporarily Unavailable`);
|
||||
handleError(req, res, 503, `Service Temporarily Unavailable`);
|
||||
}
|
||||
} 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);
|
||||
res.send(txIdResult);
|
||||
} 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'));
|
||||
}
|
||||
}
|
||||
@ -763,7 +794,7 @@ class BitcoinRoutes {
|
||||
const txIdResult = await bitcoinClient.sendRawTransaction(txHex);
|
||||
res.send(txIdResult);
|
||||
} 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'));
|
||||
}
|
||||
}
|
||||
@ -775,8 +806,20 @@ class BitcoinRoutes {
|
||||
const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate);
|
||||
res.send(result);
|
||||
} catch (e: any) {
|
||||
res.setHeader('content-type', 'text/plain');
|
||||
res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
|
||||
handleError(req, res, 400, e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, 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'));
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact
|
||||
import { IEsploraApi } from './esplora-api.interface';
|
||||
import logger from '../../logger';
|
||||
import { Common } from '../common';
|
||||
import { TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||
import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
|
||||
|
||||
interface FailoverHost {
|
||||
host: string,
|
||||
@ -305,7 +305,7 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
}
|
||||
|
||||
$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[]> {
|
||||
@ -332,6 +332,10 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||
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> {
|
||||
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 mempool from './mempool';
|
||||
import CpfpRepository from '../repositories/CpfpRepository';
|
||||
import accelerationApi from './services/acceleration';
|
||||
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
|
||||
|
||||
class Blocks {
|
||||
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 {
|
||||
id: hash,
|
||||
transactions: Common.classifyTransactions(transactions),
|
||||
transactions: Common.classifyTransactions(transactions, height),
|
||||
};
|
||||
}
|
||||
|
||||
@ -341,7 +343,12 @@ class Blocks {
|
||||
id: pool.uniqueId,
|
||||
name: pool.name,
|
||||
slug: pool.slug,
|
||||
minerNames: null,
|
||||
};
|
||||
|
||||
if (extras.pool.name === 'OCEAN') {
|
||||
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
|
||||
}
|
||||
}
|
||||
|
||||
extras.matchRate = null;
|
||||
@ -405,8 +412,16 @@ class Blocks {
|
||||
}
|
||||
|
||||
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
|
||||
const indexedBlocks = await blocksRepository.$getIndexedBlocks();
|
||||
const indexedBlocks = (await blocksRepository.$getIndexedBlocks()).filter(block => block.height >= lastBlockToIndex);
|
||||
const indexedBlockSummariesHashesArray = await BlocksSummariesRepository.$getIndexedSummariesId();
|
||||
|
||||
const indexedBlockSummariesHashes = {}; // Use a map for faster seek during the indexing loop
|
||||
@ -439,7 +454,7 @@ class Blocks {
|
||||
|
||||
|
||||
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);
|
||||
if (cpfpSummary) {
|
||||
await this.$getStrippedBlockTransactions(block.hash, true, true, cpfpSummary, block.height); // This will index the block summary
|
||||
@ -615,7 +630,7 @@ class Blocks {
|
||||
// add CPFP
|
||||
const cpfpSummary = calculateGoodBlockCpfp(height, txs, []);
|
||||
// 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);
|
||||
if (unclassifiedBlocks[height].version < 2 && targetSummaryVersion === 2) {
|
||||
const cpfpClusters = await CpfpRepository.$getClustersAt(height);
|
||||
@ -652,7 +667,7 @@ class Blocks {
|
||||
}
|
||||
const cpfpSummary = calculateGoodBlockCpfp(height, templateTxs?.filter(tx => tx['effectiveFeePerVsize'] != null) as MempoolTransactionExtended[], []);
|
||||
// classify
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, cpfpSummary.transactions);
|
||||
const { transactions: classifiedTxs } = this.summarizeBlockTransactions(blockHash, height, cpfpSummary.transactions);
|
||||
const classifiedTxMap: { [txid: string]: TransactionClassified } = {};
|
||||
for (const tx of classifiedTxs) {
|
||||
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 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}`);
|
||||
|
||||
if (Common.indexingEnabled()) {
|
||||
@ -927,12 +947,12 @@ class Blocks {
|
||||
const newBlock = await this.$indexBlock(lastBlock.height - i);
|
||||
this.blocks.push(newBlock);
|
||||
this.updateTimerProgress(timer, `reindexed block`);
|
||||
let cpfpSummary;
|
||||
let newCpfpSummary;
|
||||
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`);
|
||||
}
|
||||
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`);
|
||||
}
|
||||
await mining.$indexDifficultyAdjustments();
|
||||
@ -981,7 +1001,7 @@ class Blocks {
|
||||
|
||||
// start async callbacks
|
||||
this.updateTimerProgress(timer, `starting async callbacks for ${this.currentBlockHeight}`);
|
||||
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, transactions));
|
||||
const callbackPromises = this.newAsyncBlockCallbacks.map((cb) => cb(blockExtended, txIds, cpfpSummary.transactions));
|
||||
|
||||
if (block.height % 2016 === 0) {
|
||||
if (Common.indexingEnabled()) {
|
||||
@ -1163,7 +1183,7 @@ class Blocks {
|
||||
transactions: cpfpSummary.transactions.map(tx => {
|
||||
let flags: number = 0;
|
||||
try {
|
||||
flags = Common.getTransactionFlags(tx);
|
||||
flags = Common.getTransactionFlags(tx, height);
|
||||
} catch (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 {
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
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;
|
||||
} else {
|
||||
// Call Core RPC
|
||||
@ -1204,6 +1224,11 @@ class Blocks {
|
||||
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
|
||||
*
|
||||
@ -1318,7 +1343,7 @@ class Blocks {
|
||||
let summaryVersion = 0;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
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;
|
||||
} else {
|
||||
// Call Core RPC
|
||||
@ -1397,11 +1422,11 @@ class Blocks {
|
||||
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;
|
||||
if (!transactions) {
|
||||
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) {
|
||||
const block = await bitcoinClient.getBlock(hash, 2);
|
||||
@ -1413,7 +1438,7 @@ class Blocks {
|
||||
}
|
||||
|
||||
if (transactions?.length != null) {
|
||||
const summary = calculateFastBlockCpfp(height, transactions as TransactionExtended[]);
|
||||
const summary = calculateFastBlockCpfp(height, transactions);
|
||||
|
||||
await this.$saveCpfp(hash, height, summary);
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as bitcoinjs from 'bitcoinjs-lib';
|
||||
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 { NodeSocket } from '../repositories/NodesSocketsRepository';
|
||||
import { isIP } from 'net';
|
||||
@ -10,7 +10,6 @@ import logger from '../logger';
|
||||
import { getVarIntLength, opcodes, parseMultisigScript } from '../utils/bitcoin-script';
|
||||
|
||||
// Bitcoin Core default policy settings
|
||||
const TX_MAX_STANDARD_VERSION = 2;
|
||||
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
||||
const MAX_BLOCK_SIGOPS_COST = 80_000;
|
||||
const MAX_STANDARD_TX_SIGOPS_COST = (MAX_BLOCK_SIGOPS_COST / 5);
|
||||
@ -80,8 +79,8 @@ export class Common {
|
||||
return arr;
|
||||
}
|
||||
|
||||
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: MempoolTransactionExtended[] } {
|
||||
const matches: { [txid: string]: MempoolTransactionExtended[] } = {};
|
||||
static findRbfTransactions(added: MempoolTransactionExtended[], deleted: MempoolTransactionExtended[], forceScalable = false): { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} {
|
||||
const matches: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }} = {};
|
||||
|
||||
// For small N, a naive nested loop is extremely fast, but it doesn't scale
|
||||
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));
|
||||
});
|
||||
if (foundMatches?.length) {
|
||||
matches[addedTx.txid] = [...new Set(foundMatches)];
|
||||
matches[addedTx.txid] = { replaced: [...new Set(foundMatches)], replacedBy: addedTx };
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -124,7 +123,7 @@ export class Common {
|
||||
foundMatches.add(deletedTx);
|
||||
}
|
||||
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();
|
||||
for (let i = 0; i < tx.vin.length; 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) {
|
||||
replaced.add(match);
|
||||
// remove this tx from the spendMap
|
||||
// prevents the same tx being replaced more than once
|
||||
for (const replacedVin of match.vin) {
|
||||
const key = `${replacedVin.txid}:${replacedVin.vout}`;
|
||||
spendMap.delete(key);
|
||||
const replacedKey = `${replacedVin.txid}:${replacedVin.vout}`;
|
||||
spendMap.delete(replacedKey);
|
||||
}
|
||||
}
|
||||
const key = `${vin.txid}:${vin.vout}`;
|
||||
spendMap.delete(key);
|
||||
}
|
||||
if (replaced.size) {
|
||||
@ -200,10 +199,13 @@ export class Common {
|
||||
*
|
||||
* 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)
|
||||
*
|
||||
* 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
|
||||
if (tx.version > TX_MAX_STANDARD_VERSION) {
|
||||
if (this.isNonStandardVersion(tx, height)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -250,6 +252,8 @@ export class Common {
|
||||
}
|
||||
} else if (['unknown', 'provably_unspendable', 'empty'].includes(vin.prevout?.scriptpubkey_type || '')) {
|
||||
return true;
|
||||
} else if (this.isNonStandardAnchor(tx, height)) {
|
||||
return true;
|
||||
}
|
||||
// TODO: bad-witness-nonstandard
|
||||
}
|
||||
@ -335,6 +339,49 @@ export class Common {
|
||||
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 {
|
||||
let weight = tx.weight;
|
||||
let hasWitness = false;
|
||||
@ -415,7 +462,7 @@ export class Common {
|
||||
return flags;
|
||||
}
|
||||
|
||||
static getTransactionFlags(tx: TransactionExtended): number {
|
||||
static getTransactionFlags(tx: TransactionExtended, height?: number): number {
|
||||
let flags = tx.flags ? BigInt(tx.flags) : 0n;
|
||||
|
||||
// Update variable flags (CPFP, RBF)
|
||||
@ -548,7 +595,7 @@ export class Common {
|
||||
if (hasFakePubkey) {
|
||||
flags |= TransactionFlags.fake_pubkey;
|
||||
}
|
||||
|
||||
|
||||
// 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)
|
||||
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;
|
||||
}
|
||||
|
||||
if (this.isNonStandard(tx)) {
|
||||
if (this.isNonStandard(tx, height)) {
|
||||
flags |= TransactionFlags.nonstandard;
|
||||
}
|
||||
|
||||
return Number(flags);
|
||||
}
|
||||
|
||||
static classifyTransaction(tx: TransactionExtended): TransactionClassified {
|
||||
static classifyTransaction(tx: TransactionExtended, height?: number): TransactionClassified {
|
||||
let flags = 0;
|
||||
try {
|
||||
flags = Common.getTransactionFlags(tx);
|
||||
flags = Common.getTransactionFlags(tx, height);
|
||||
} catch (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[] {
|
||||
return txs.map(Common.classifyTransaction);
|
||||
static classifyTransactions(txs: TransactionExtended[], height?: number): TransactionClassified[] {
|
||||
return txs.map(tx => Common.classifyTransaction(tx, height));
|
||||
}
|
||||
|
||||
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 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 clusterMap: { [txid: string]: CpfpCluster } = {}; // map transactions to their cpfp cluster
|
||||
let clusterTxs: TransactionExtended[] = []; // working list of elements of the current cluster
|
||||
@ -93,6 +93,7 @@ export function calculateFastBlockCpfp(height: number, transactions: Transaction
|
||||
return {
|
||||
transactions,
|
||||
clusters,
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
|
||||
@ -159,6 +160,7 @@ export function calculateGoodBlockCpfp(height: number, transactions: MempoolTran
|
||||
return {
|
||||
transactions: transactions.map(tx => txMap[tx.txid]),
|
||||
clusters: clusterArray,
|
||||
version: 2,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
||||
import { RowDataPacket } from 'mysql2';
|
||||
|
||||
class DatabaseMigration {
|
||||
private static currentVersion = 80;
|
||||
private static currentVersion = 93;
|
||||
private queryTimeout = 3600_000;
|
||||
private statisticsAddedIndexed = false;
|
||||
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 `ZAR` float DEFAULT "-1"');
|
||||
|
||||
await this.$executeQuery('TRUNCATE hashrates');
|
||||
await this.$executeQuery('TRUNCATE difficulty_adjustments');
|
||||
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
|
||||
if (isBitcoin === true) {
|
||||
await this.$executeQuery('TRUNCATE hashrates');
|
||||
await this.$executeQuery('TRUNCATE difficulty_adjustments');
|
||||
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
|
||||
}
|
||||
|
||||
await this.updateToSchemaVersion(75);
|
||||
}
|
||||
@ -691,6 +693,114 @@ class DatabaseMigration {
|
||||
await this.$executeQuery('ALTER TABLE `blocks` ADD coinbase_addresses JSON DEFAULT NULL');
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
@ -257,6 +257,7 @@ class DiskCache {
|
||||
trees: rbfData.rbf.trees,
|
||||
expiring: rbfData.rbf.expiring.map(([txid, value]) => ({ key: txid, value })),
|
||||
mempool: memPool.getMempool(),
|
||||
spendMap: memPool.getSpendMap(),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import config from '../../config';
|
||||
import { Application, Request, Response } from 'express';
|
||||
import channelsApi from './channels.api';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class ChannelsRoutes {
|
||||
constructor() { }
|
||||
@ -22,7 +23,7 @@ class ChannelsRoutes {
|
||||
const channels = await channelsApi.$searchChannelsById(req.params.search);
|
||||
res.json(channels);
|
||||
} 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.json(channel);
|
||||
} 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 : '';
|
||||
|
||||
if (index < -1) {
|
||||
res.status(400).send('Invalid index');
|
||||
handleError(req, res, 400, 'Invalid index');
|
||||
return;
|
||||
}
|
||||
if (['open', 'active', 'closed'].includes(status) === false) {
|
||||
res.status(400).send('Invalid status');
|
||||
handleError(req, res, 400, 'Invalid status');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -69,14 +70,14 @@ class ChannelsRoutes {
|
||||
res.header('X-Total-Count', channelsCount.toString());
|
||||
res.json(channels);
|
||||
} 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> {
|
||||
try {
|
||||
if (!Array.isArray(req.query.txId)) {
|
||||
res.status(400).send('Not an array');
|
||||
handleError(req, res, 400, 'Not an array');
|
||||
return;
|
||||
}
|
||||
const txIds: string[] = [];
|
||||
@ -107,7 +108,7 @@ class ChannelsRoutes {
|
||||
|
||||
res.json(result);
|
||||
} 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.json(channels);
|
||||
} 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.json(channels);
|
||||
} 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 channelsApi from './channels.api';
|
||||
import statisticsApi from './statistics.api';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class GeneralLightningRoutes {
|
||||
constructor() { }
|
||||
|
||||
@ -27,7 +29,7 @@ class GeneralLightningRoutes {
|
||||
channels: channels,
|
||||
});
|
||||
} 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.json(statistics);
|
||||
} 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();
|
||||
res.json(statistics);
|
||||
} 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 DB from '../../database';
|
||||
import { INodesRanking } from '../../mempool.interfaces';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class NodesRoutes {
|
||||
constructor() { }
|
||||
@ -31,7 +32,7 @@ class NodesRoutes {
|
||||
const nodes = await nodesApi.$searchNodeByPublicKeyOrAlias(req.params.search);
|
||||
res.json(nodes);
|
||||
} 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) {}
|
||||
}
|
||||
|
||||
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(nodes);
|
||||
} 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 {
|
||||
const node = await nodesApi.$getNode(req.params.public_key);
|
||||
if (!node) {
|
||||
res.status(404).send('Node not found');
|
||||
handleError(req, res, 404, 'Node not found');
|
||||
return;
|
||||
}
|
||||
res.header('Pragma', 'public');
|
||||
@ -203,7 +204,7 @@ class NodesRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(node);
|
||||
} 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.json(statistics);
|
||||
} 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 {
|
||||
const node = await nodesApi.$getFeeHistogram(req.params.public_key);
|
||||
if (!node) {
|
||||
res.status(404).send('Node not found');
|
||||
handleError(req, res, 404, 'Node not found');
|
||||
return;
|
||||
}
|
||||
res.header('Pragma', 'public');
|
||||
@ -231,7 +232,7 @@ class NodesRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(node);
|
||||
} 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,
|
||||
});
|
||||
} 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.json(topCapacityNodes);
|
||||
} 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.json(topCapacityNodes);
|
||||
} 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.json(topCapacityNodes);
|
||||
} 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.json(nodesPerAs);
|
||||
} 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.json(worldNodes);
|
||||
} 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -335,7 +336,7 @@ class NodesRoutes {
|
||||
nodes: nodes,
|
||||
});
|
||||
} 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) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -362,7 +363,7 @@ class NodesRoutes {
|
||||
nodes: nodes,
|
||||
});
|
||||
} 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.json(nodesPerAs);
|
||||
} 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 elementsParser from './elements-parser';
|
||||
import icons from './icons';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class LiquidRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
@ -42,7 +43,7 @@ class LiquidRoutes {
|
||||
res.setHeader('content-length', result.length);
|
||||
res.send(result);
|
||||
} 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) {
|
||||
res.json(result);
|
||||
} 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.json(pegs);
|
||||
} 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.json(reserves);
|
||||
} 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.json(currentSupply);
|
||||
} 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.json(currentReserves);
|
||||
} 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.json(auditStatus);
|
||||
} 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.json(federationAddresses);
|
||||
} 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.json(federationAddresses);
|
||||
} 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.json(federationUtxos);
|
||||
} 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.json(expiredUtxos);
|
||||
} 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.json(federationUtxos);
|
||||
} 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.json(emergencySpentUtxos);
|
||||
} 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.json(emergencySpentUtxos);
|
||||
} 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.json(recentPegs);
|
||||
} 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.json(pegsVolume);
|
||||
} 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.json(pegsCount);
|
||||
} 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;
|
||||
let hasBlockStack = blocks.length >= 8;
|
||||
let stackWeight;
|
||||
let feeStatsCalculator: OnlineFeeStatsCalculator | void;
|
||||
let feeStatsCalculator: OnlineFeeStatsCalculator | null = null;
|
||||
if (hasBlockStack) {
|
||||
if (blockWeights && blockWeights[7] !== null) {
|
||||
stackWeight = blockWeights[7];
|
||||
@ -380,28 +380,36 @@ class MempoolBlocks {
|
||||
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 memberTxid of cluster) {
|
||||
const mempoolTx = mempool[memberTxid];
|
||||
if (mempoolTx) {
|
||||
const ancestors: Ancestor[] = [];
|
||||
const descendants: Ancestor[] = [];
|
||||
// ugly micro-optimization to avoid allocating new arrays
|
||||
ancestors.length = 0;
|
||||
descendants.length = 0;
|
||||
let matched = false;
|
||||
cluster.forEach(txid => {
|
||||
ancestor = mempool[txid];
|
||||
if (txid === memberTxid) {
|
||||
matched = true;
|
||||
} else {
|
||||
if (!mempool[txid]) {
|
||||
if (!ancestor) {
|
||||
console.log('txid missing from mempool! ', txid, candidates?.txs[txid]);
|
||||
return;
|
||||
}
|
||||
const relative = {
|
||||
txid: txid,
|
||||
fee: mempool[txid].fee,
|
||||
weight: (mempool[txid].adjustedVsize * 4),
|
||||
fee: ancestor.fee,
|
||||
weight: (ancestor.adjustedVsize * 4),
|
||||
};
|
||||
if (matched) {
|
||||
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 {
|
||||
ancestors.push(relative);
|
||||
}
|
||||
@ -410,7 +418,20 @@ class MempoolBlocks {
|
||||
if (mempoolTx.ancestors?.length !== ancestors.length || mempoolTx.descendants?.length !== descendants.length) {
|
||||
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;
|
||||
// update this thread's mempool with the results
|
||||
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 totalVsize = 0;
|
||||
let totalWeight = 0;
|
||||
@ -436,8 +460,9 @@ class MempoolBlocks {
|
||||
}
|
||||
}
|
||||
|
||||
for (const txid of block) {
|
||||
if (txid) {
|
||||
for (let i = 0; i < block.length; i++) {
|
||||
const txid = block[i];
|
||||
if (txid in mempool) {
|
||||
mempoolTx = mempool[txid];
|
||||
// save position in projected blocks
|
||||
mempoolTx.position = {
|
||||
@ -445,28 +470,40 @@ class MempoolBlocks {
|
||||
vsize: totalVsize + (mempoolTx.vsize / 2),
|
||||
};
|
||||
|
||||
const acceleration = accelerations[txid];
|
||||
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
||||
if (!mempoolTx.acceleration) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
}
|
||||
mempoolTx.acceleration = true;
|
||||
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
|
||||
mempoolTx.acceleratedAt = acceleration?.added;
|
||||
for (const ancestor of mempoolTx.ancestors || []) {
|
||||
if (!mempool[ancestor.txid].acceleration) {
|
||||
mempool[ancestor.txid].cpfpDirty = true;
|
||||
if (txid in accelerations) {
|
||||
acceleration = accelerations[txid];
|
||||
if (isAcceleratedBy[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
||||
if (!mempoolTx.acceleration) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
}
|
||||
mempoolTx.acceleration = true;
|
||||
mempoolTx.acceleratedBy = isAcceleratedBy[txid] || acceleration?.pools;
|
||||
mempoolTx.acceleratedAt = acceleration?.added;
|
||||
mempoolTx.feeDelta = acceleration?.feeDelta;
|
||||
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 {
|
||||
if (mempoolTx.acceleration) {
|
||||
mempoolTx.cpfpDirty = true;
|
||||
delete mempoolTx.acceleration;
|
||||
}
|
||||
delete mempoolTx.acceleration;
|
||||
}
|
||||
|
||||
// online calculation of stack-of-blocks fee stats
|
||||
@ -484,7 +521,7 @@ class MempoolBlocks {
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.dataToMempoolBlocks(
|
||||
mempoolBlocks[blockIndex] = this.dataToMempoolBlocks(
|
||||
block,
|
||||
transactions,
|
||||
totalSize,
|
||||
@ -492,7 +529,7 @@ class MempoolBlocks {
|
||||
totalFees,
|
||||
(hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) ? feeStatsCalculator.getRawFeeStats() : undefined,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (saveResults) {
|
||||
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 };
|
||||
} = {};
|
||||
// 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;
|
||||
for (const ancestor of mempoolCache[acc.txid].ancestors || []) {
|
||||
vsize += (ancestor.weight / 4);
|
||||
|
@ -10,6 +10,7 @@ import bitcoinClient from './bitcoin/bitcoin-client';
|
||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import rbfCache from './rbf-cache';
|
||||
import { Acceleration } from './services/acceleration';
|
||||
import accelerationApi from './services/acceleration';
|
||||
import redisCache from './redis-cache';
|
||||
import blocks from './blocks';
|
||||
|
||||
@ -19,12 +20,13 @@ class Mempool {
|
||||
private mempoolCache: { [txId: string]: MempoolTransactionExtended } = {};
|
||||
private mempoolCandidates: { [txid: string ]: boolean } = {};
|
||||
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,
|
||||
maxmempool: 300000000, mempoolminfee: Common.isLiquid() ? 0.00000100 : 0.00001000, minrelaytxfee: Common.isLiquid() ? 0.00000100 : 0.00001000 };
|
||||
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[],
|
||||
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
|
||||
deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[], candidates?: GbtCandidates) => Promise<void>) | undefined;
|
||||
|
||||
private accelerations: { [txId: string]: Acceleration } = {};
|
||||
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; },
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[]) => void): void {
|
||||
this.mempoolChangedCallback = fn;
|
||||
}
|
||||
|
||||
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 {
|
||||
this.$asyncMempoolChangedCallback = fn;
|
||||
}
|
||||
@ -206,7 +208,7 @@ class Mempool {
|
||||
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...`);
|
||||
|
||||
// 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));
|
||||
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) {
|
||||
hasChange = true;
|
||||
}
|
||||
@ -362,12 +364,15 @@ class Mempool {
|
||||
|
||||
const candidatesChanged = candidates?.added?.length || candidates?.removed?.length;
|
||||
|
||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
|
||||
this.recentlyDeleted.unshift(deletedTransactions);
|
||||
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');
|
||||
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');
|
||||
}
|
||||
|
||||
@ -395,58 +400,11 @@ class Mempool {
|
||||
return this.accelerations;
|
||||
}
|
||||
|
||||
public $updateAccelerations(newAccelerations: Acceleration[]): string[] {
|
||||
public updateAccelerations(newAccelerationMap: Record<string, Acceleration>): string[] {
|
||||
try {
|
||||
const changed: string[] = [];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const accelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, newAccelerationMap);
|
||||
this.accelerations = newAccelerationMap;
|
||||
|
||||
return changed;
|
||||
return accelerationDelta;
|
||||
} catch (e: any) {
|
||||
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
|
||||
return [];
|
||||
@ -541,16 +499,7 @@ class Mempool {
|
||||
}
|
||||
}
|
||||
|
||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: MempoolTransactionExtended[]; }): 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 {
|
||||
public handleRbfTransactions(rbfTransactions: { [txid: string]: { replaced: MempoolTransactionExtended[], replacedBy: TransactionExtended }}): void {
|
||||
for (const rbfTransaction in rbfTransactions) {
|
||||
if (rbfTransactions[rbfTransaction].replacedBy && rbfTransactions[rbfTransaction]?.replaced?.length) {
|
||||
// Store replaced transactions
|
||||
|
@ -337,7 +337,7 @@ export function makeBlockTemplate(candidates: MempoolTransactionExtended[], acce
|
||||
let failures = 0;
|
||||
while (mempoolArray.length || modified.length) {
|
||||
// skip invalid transactions
|
||||
while (mempoolArray[0].used || mempoolArray[0].modified) {
|
||||
while (mempoolArray[0]?.used || mempoolArray[0]?.modified) {
|
||||
mempoolArray.shift();
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import mining from "./mining";
|
||||
import PricesRepository from '../../repositories/PricesRepository';
|
||||
import AccelerationRepository from '../../repositories/AccelerationRepository';
|
||||
import accelerationApi from '../services/acceleration';
|
||||
import { handleError } from '../../utils/api';
|
||||
|
||||
class MiningRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
@ -53,12 +54,12 @@ class MiningRoutes {
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
|
||||
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;
|
||||
}
|
||||
const timestamp = parseInt(req.query.timestamp as string, 10) || 0;
|
||||
const currency = req.query.currency as string;
|
||||
|
||||
|
||||
let response;
|
||||
if (timestamp && currency) {
|
||||
response = await PricesRepository.$getNearestHistoricalPrice(timestamp, currency);
|
||||
@ -71,7 +72,7 @@ class MiningRoutes {
|
||||
}
|
||||
res.status(200).send(response);
|
||||
} 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);
|
||||
} catch (e) {
|
||||
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 {
|
||||
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);
|
||||
} catch (e) {
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
} 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.json(stats);
|
||||
} 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.json(hashrates);
|
||||
} 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);
|
||||
} catch (e) {
|
||||
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 {
|
||||
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) {
|
||||
let currentHashrate = 0, currentDifficulty = 0;
|
||||
try {
|
||||
currentHashrate = await bitcoinClient.getNetworkHashPs();
|
||||
currentHashrate = await bitcoinClient.getNetworkHashPs(1008);
|
||||
currentDifficulty = await bitcoinClient.getDifficulty();
|
||||
} catch (e) {
|
||||
logger.debug('Bitcoin Core is not available, using zeroed value for current hashrate and difficulty');
|
||||
@ -203,7 +204,7 @@ class MiningRoutes {
|
||||
currentDifficulty: currentDifficulty,
|
||||
});
|
||||
} 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.json(blockFees);
|
||||
} 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.json(blockFees);
|
||||
} 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.json(blockRewards);
|
||||
} 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.json(blockFeeRates);
|
||||
} 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
|
||||
});
|
||||
} 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.json(difficulty.map(adj => [adj.time, adj.height, adj.difficulty, adj.adjustment]));
|
||||
} 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.json(blocksHealth.map(health => [health.time, health.height, health.match_rate]));
|
||||
} 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);
|
||||
|
||||
if (!audit) {
|
||||
res.status(204).send(`This block has not been audited.`);
|
||||
handleError(req, res, 204, `This block has not been audited.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -335,7 +336,7 @@ class MiningRoutes {
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||
res.json(audit);
|
||||
} 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.json(result);
|
||||
} 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.json(await BlocksAuditsRepository.$getBlockAuditScores(height, height - 15));
|
||||
} 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.json(audit || 'null');
|
||||
} 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
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;
|
||||
}
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(req.params.slug));
|
||||
} 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.setHeader('Expires', new Date(Date.now() + 1000 * 3600 * 24).toUTCString());
|
||||
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;
|
||||
}
|
||||
const height = req.params.height === undefined ? undefined : parseInt(req.params.height, 10);
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, height));
|
||||
} 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
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;
|
||||
}
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationInfo(null, null, req.params.interval));
|
||||
} 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
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;
|
||||
}
|
||||
res.status(200).send(await AccelerationRepository.$getAccelerationTotals(<string>req.query.pool, <string>req.query.interval));
|
||||
} 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.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
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;
|
||||
}
|
||||
res.status(200).send(accelerationApi.accelerations || []);
|
||||
res.status(200).send(Object.values(accelerationApi.getAccelerations() || {}));
|
||||
} 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> {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS || config.MEMPOOL.OFFICIAL) {
|
||||
res.status(405).send('not available.');
|
||||
return;
|
||||
}
|
||||
res.setHeader('Pragma', 'no-cache');
|
||||
res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0');
|
||||
res.setHeader('expires', -1);
|
||||
try {
|
||||
accelerationApi.accelerationRequested(req.params.txid);
|
||||
res.status(200).send('ok');
|
||||
res.status(200).send();
|
||||
} 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;
|
||||
|
||||
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 {
|
||||
poolsStatistics['lastEstimatedHashrate'] = await bitcoinClient.getNetworkHashPs(totalBlock24h);
|
||||
poolsStatistics['lastEstimatedHashrate3d'] = await bitcoinClient.getNetworkHashPs(totalBlock3d);
|
||||
poolsStatistics['lastEstimatedHashrate1w'] = await bitcoinClient.getNetworkHashPs(totalBlock1w);
|
||||
} catch (e) {
|
||||
poolsStatistics['lastEstimatedHashrate'] = 0;
|
||||
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 config from '../../config';
|
||||
import pricesUpdater from '../../tasks/price-updater';
|
||||
import logger from '../../logger';
|
||||
import PricesRepository from '../../repositories/PricesRepository';
|
||||
|
||||
class PricesRoutes {
|
||||
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 {
|
||||
@ -14,6 +19,23 @@ class PricesRoutes {
|
||||
|
||||
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();
|
||||
|
@ -44,6 +44,22 @@ interface CacheEvent {
|
||||
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 {
|
||||
private replacedBy: 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Low level cache operations
|
||||
*/
|
||||
|
||||
private addTx(txid: string, tx: MempoolTransactionExtended): void {
|
||||
this.txs.set(txid, tx);
|
||||
this.cacheQueue.push({ op: CacheOp.Add, type: 'tx', txid });
|
||||
@ -92,6 +112,12 @@ class RbfCache {
|
||||
this.cacheQueue.push({ op: CacheOp.Remove, type: 'exp', txid });
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic data structure operations
|
||||
* must uphold tree invariants
|
||||
*/
|
||||
|
||||
|
||||
public add(replaced: MempoolTransactionExtended[], newTxExtended: MempoolTransactionExtended): void {
|
||||
if (!newTxExtended || !replaced?.length || this.txs.has(newTxExtended.txid)) {
|
||||
return;
|
||||
@ -114,6 +140,10 @@ class RbfCache {
|
||||
if (!replacedTx.rbf) {
|
||||
txFullRbf = true;
|
||||
}
|
||||
if (this.replacedBy.has(replacedTx.txid)) {
|
||||
// should never happen
|
||||
continue;
|
||||
}
|
||||
this.replacedBy.set(replacedTx.txid, newTx.txid);
|
||||
if (this.treeMap.has(replacedTx.txid)) {
|
||||
const treeId = this.treeMap.get(replacedTx.txid);
|
||||
@ -140,18 +170,47 @@ class RbfCache {
|
||||
}
|
||||
}
|
||||
newTx.fullRbf = txFullRbf;
|
||||
const treeId = replacedTrees[0].tx.txid;
|
||||
const newTree = {
|
||||
tx: newTx,
|
||||
time: newTime,
|
||||
fullRbf: treeFullRbf,
|
||||
replaces: replacedTrees
|
||||
};
|
||||
this.addTree(treeId, newTree);
|
||||
this.updateTreeMap(treeId, newTree);
|
||||
this.addTree(newTree.tx.txid, newTree);
|
||||
this.updateTreeMap(newTree.tx.txid, newTree);
|
||||
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 {
|
||||
return this.txs.has(txId);
|
||||
}
|
||||
@ -232,32 +291,6 @@ class RbfCache {
|
||||
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?
|
||||
public isFullRbf(txid: string): boolean {
|
||||
const treeId = this.treeMap.get(txid);
|
||||
@ -271,6 +304,10 @@ class RbfCache {
|
||||
return tree?.fullRbf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache maintenance & utility functions
|
||||
*/
|
||||
|
||||
private cleanup(): void {
|
||||
const now = Date.now();
|
||||
for (const txid of this.expiring.keys()) {
|
||||
@ -299,10 +336,6 @@ class RbfCache {
|
||||
for (const tx of (replaces || [])) {
|
||||
// recursively remove prior versions from the cache
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
txs.forEach(txEntry => {
|
||||
this.txs.set(txEntry.value.txid, txEntry.value);
|
||||
});
|
||||
this.staleCount = 0;
|
||||
for (const deflatedTree of trees) {
|
||||
await this.importTree(mempool, deflatedTree.root, deflatedTree.root, deflatedTree, this.txs);
|
||||
for (const deflatedTree of trees.sort((a, b) => Object.keys(b).length - Object.keys(a).length)) {
|
||||
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 => {
|
||||
if (this.txs.has(expiringEntry.key)) {
|
||||
@ -385,6 +425,31 @@ class RbfCache {
|
||||
}
|
||||
});
|
||||
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();
|
||||
logger.debug(`loaded ${txs.length} txs, ${trees.length} trees into rbf cache, ${expiring.length} due to expire, ${this.staleCount} were stale`);
|
||||
this.cleanup();
|
||||
@ -426,6 +491,12 @@ class RbfCache {
|
||||
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
|
||||
for (const childId of treeInfo.replaces) {
|
||||
const replaced = await this.importTree(mempool, root, childId, deflated, txs, mined);
|
||||
@ -457,10 +528,6 @@ class RbfCache {
|
||||
fullRbf: treeInfo.fullRbf,
|
||||
replaces,
|
||||
};
|
||||
this.treeMap.set(txid, root);
|
||||
if (root === txid) {
|
||||
this.addTree(root, tree);
|
||||
}
|
||||
return tree;
|
||||
}
|
||||
|
||||
@ -511,6 +578,7 @@ class RbfCache {
|
||||
processTxs(txs);
|
||||
}
|
||||
|
||||
// evict missing transactions
|
||||
for (const txid of txids) {
|
||||
if (!found[txid]) {
|
||||
this.evict(txid, false);
|
||||
|
@ -365,6 +365,7 @@ class RedisCache {
|
||||
trees: rbfTrees.map(loadedTree => { loadedTree.value.key = loadedTree.key; return loadedTree.value; }),
|
||||
expiring: rbfExpirations,
|
||||
mempool: memPool.getMempool(),
|
||||
spendMap: memPool.getSpendMap(),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { WebSocket } from 'ws';
|
||||
import config from '../../config';
|
||||
import logger from '../../logger';
|
||||
import { BlockExtended } from '../../mempool.interfaces';
|
||||
import axios from 'axios';
|
||||
import mempool from '../mempool';
|
||||
import websocketHandler from '../websocket-handler';
|
||||
|
||||
type MyAccelerationStatus = 'requested' | 'accelerating' | 'done';
|
||||
|
||||
@ -37,13 +40,23 @@ export interface AccelerationHistory {
|
||||
};
|
||||
|
||||
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 _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 lastPing = Date.now();
|
||||
private lastPong = Date.now();
|
||||
private forcePoll = false;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -52,7 +65,9 @@ class AccelerationApi {
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -69,11 +84,18 @@ class AccelerationApi {
|
||||
}
|
||||
}
|
||||
|
||||
public async $updateAccelerations(): Promise<Acceleration[] | null> {
|
||||
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||
public async $updateAccelerations(): Promise<Record<string, Acceleration> | null> {
|
||||
if (this.useWebsocket && this.websocketConnected) {
|
||||
return this._accelerations;
|
||||
}
|
||||
if (!this.onDemandPollingEnabled) {
|
||||
const accelerations = await this.$fetchAccelerations();
|
||||
if (accelerations) {
|
||||
this._accelerations = accelerations;
|
||||
const latestAccelerations = {};
|
||||
for (const acc of accelerations) {
|
||||
latestAccelerations[acc.txid] = acc;
|
||||
}
|
||||
this._accelerations = latestAccelerations;
|
||||
return this._accelerations;
|
||||
}
|
||||
} else {
|
||||
@ -82,7 +104,7 @@ class AccelerationApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async $updateAccelerationsOnDemand(): Promise<Acceleration[] | null> {
|
||||
private async $updateAccelerationsOnDemand(): Promise<Record<string, Acceleration> | null> {
|
||||
const shouldUpdate = this.forcePoll
|
||||
|| this.countMyAccelerationsWithStatus('requested') > 0
|
||||
|| (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;
|
||||
}
|
||||
|
||||
@ -149,6 +175,148 @@ class AccelerationApi {
|
||||
}
|
||||
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();
|
@ -8,7 +8,15 @@ import { TransactionExtended } from '../../mempool.interfaces';
|
||||
interface WalletAddress {
|
||||
address: string;
|
||||
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 {
|
||||
@ -22,7 +30,7 @@ interface Wallet extends WalletConfig {
|
||||
lastPoll: number;
|
||||
}
|
||||
|
||||
const POLL_FREQUENCY = 60 * 60 * 1000; // 1 hour
|
||||
const POLL_FREQUENCY = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
class WalletApi {
|
||||
private wallets: Record<string, Wallet> = {};
|
||||
@ -39,8 +47,11 @@ class WalletApi {
|
||||
return this.wallets?.[wallet]?.addresses || {};
|
||||
}
|
||||
|
||||
// resync wallet addresses from the provided API
|
||||
// resync wallet addresses from the services backend
|
||||
async $syncWallets(): Promise<void> {
|
||||
if (!config.WALLETS.ENABLED || this.syncing) {
|
||||
return;
|
||||
}
|
||||
this.syncing = true;
|
||||
for (const walletKey of Object.keys(this.wallets)) {
|
||||
const wallet = this.wallets[walletKey];
|
||||
@ -77,10 +88,14 @@ class WalletApi {
|
||||
const refreshTransactions = !wallet.addresses[address.address] || address.active;
|
||||
if (refreshTransactions) {
|
||||
try {
|
||||
const summary = await bitcoinApi.$getAddressTransactionSummary(address.address);
|
||||
const addressInfo = await bitcoinApi.$getAddress(address.address);
|
||||
const walletAddress: WalletAddress = {
|
||||
address: address.address,
|
||||
active: address.active,
|
||||
transactions: 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}`);
|
||||
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
|
||||
processBlock(block: IEsploraApi.Block, blockTxs: TransactionExtended[]): Record<string, Record<string, IEsploraApi.AddressTxSummary[]>> {
|
||||
const walletTransactions: Record<string, Record<string, IEsploraApi.AddressTxSummary[]>> = {};
|
||||
processBlock(block: IEsploraApi.Block, blockTxs: TransactionExtended[]): Record<string, IEsploraApi.Transaction[]> {
|
||||
const walletTransactions: Record<string, IEsploraApi.Transaction[]> = {};
|
||||
for (const walletKey of Object.keys(this.wallets)) {
|
||||
const wallet = this.wallets[walletKey];
|
||||
walletTransactions[walletKey] = {};
|
||||
walletTransactions[walletKey] = [];
|
||||
for (const tx of blockTxs) {
|
||||
const funded: Record<string, number> = {};
|
||||
const spent: Record<string, number> = {};
|
||||
const fundedCount: Record<string, number> = {};
|
||||
const spentCount: Record<string, number> = {};
|
||||
let anyMatch = false;
|
||||
for (const vin of tx.vin) {
|
||||
const address = vin.prevout?.scriptpubkey_address;
|
||||
if (address && wallet.addresses[address]) {
|
||||
anyMatch = true;
|
||||
spent[address] = (spent[address] ?? 0) + (vin.prevout?.value ?? 0);
|
||||
spentCount[address] = (spentCount[address] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
for (const vout of tx.vout) {
|
||||
const address = vout.scriptpubkey_address;
|
||||
if (address && wallet.addresses[address]) {
|
||||
anyMatch = true;
|
||||
funded[address] = (funded[address] ?? 0) + (vout.value ?? 0);
|
||||
fundedCount[address] = (fundedCount[address] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
for (const address of Object.keys({ ...funded, ...spent })) {
|
||||
if (!walletTransactions[walletKey][address]) {
|
||||
walletTransactions[walletKey][address] = [];
|
||||
}
|
||||
walletTransactions[walletKey][address].push({
|
||||
// update address stats
|
||||
wallet.addresses[address].stats.tx_count++;
|
||||
wallet.addresses[address].stats.funded_txo_count += fundedCount[address] || 0;
|
||||
wallet.addresses[address].stats.spent_txo_count += spentCount[address] || 0;
|
||||
wallet.addresses[address].stats.funded_txo_sum += funded[address] || 0;
|
||||
wallet.addresses[address].stats.spent_txo_sum += spent[address] || 0;
|
||||
// add tx to summary
|
||||
const txSummary: IEsploraApi.AddressTxSummary = {
|
||||
txid: tx.txid,
|
||||
value: (funded[address] ?? 0) - (spent[address] ?? 0),
|
||||
height: block.height,
|
||||
time: block.timestamp,
|
||||
});
|
||||
};
|
||||
wallet.addresses[address].transactions?.push(txSummary);
|
||||
}
|
||||
if (anyMatch) {
|
||||
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 feePerVbytes = (transaction.fee || 0) / fractionalVsize;
|
||||
const adjustedFeePerVsize = (transaction.fee || 0) / adjustedVsize;
|
||||
const effectiveFeePerVsize = transaction['effectiveFeePerVsize'] || adjustedFeePerVsize || feePerVbytes;
|
||||
const transactionExtended: MempoolTransactionExtended = Object.assign(transaction, {
|
||||
order: this.txidToOrdering(transaction.txid),
|
||||
vsize,
|
||||
@ -128,7 +129,7 @@ class TransactionUtils {
|
||||
sigops,
|
||||
feePerVsize: feePerVbytes,
|
||||
adjustedFeePerVsize: adjustedFeePerVsize,
|
||||
effectiveFeePerVsize: adjustedFeePerVsize,
|
||||
effectiveFeePerVsize: effectiveFeePerVsize,
|
||||
});
|
||||
if (!transactionExtended?.status?.confirmed && !transactionExtended.firstSeen) {
|
||||
transactionExtended.firstSeen = Math.round((Date.now() / 1000));
|
||||
@ -338,6 +339,87 @@ class TransactionUtils {
|
||||
const positionOfScript = hasAnnex ? witness.length - 3 : witness.length - 2;
|
||||
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();
|
||||
|
@ -3,7 +3,7 @@ import * as WebSocket from 'ws';
|
||||
import {
|
||||
BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse,
|
||||
OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo,
|
||||
MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids
|
||||
MempoolDelta, MempoolDeltaTxids
|
||||
} from '../mempool.interfaces';
|
||||
import blocks from './blocks';
|
||||
import memPool from './mempool';
|
||||
@ -16,11 +16,13 @@ import transactionUtils from './transaction-utils';
|
||||
import rbfCache, { ReplacementInfo } from './rbf-cache';
|
||||
import difficultyAdjustment from './difficulty-adjustment';
|
||||
import feeApi from './fee-api';
|
||||
import BlocksRepository from '../repositories/BlocksRepository';
|
||||
import BlocksAuditsRepository from '../repositories/BlocksAuditsRepository';
|
||||
import BlocksSummariesRepository from '../repositories/BlocksSummariesRepository';
|
||||
import Audit from './audit';
|
||||
import priceUpdater from '../tasks/price-updater';
|
||||
import { ApiPrice } from '../repositories/PricesRepository';
|
||||
import { Acceleration } from './services/acceleration';
|
||||
import accelerationApi from './services/acceleration';
|
||||
import mempool from './mempool';
|
||||
import statistics from './statistics/statistics';
|
||||
@ -35,6 +37,7 @@ interface AddressTransactions {
|
||||
}
|
||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||
import { calculateMempoolTxCpfp } from './cpfp';
|
||||
import { getRecentFirstSeen } from '../utils/file-read';
|
||||
|
||||
// valid 'want' subscriptions
|
||||
const wantable = [
|
||||
@ -58,6 +61,8 @@ class WebsocketHandler {
|
||||
private lastRbfSummary: ReplacementInfo[] | null = null;
|
||||
private mempoolSequence: number = 0;
|
||||
|
||||
private accelerations: Record<string, Acceleration> = {};
|
||||
|
||||
constructor() { }
|
||||
|
||||
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 {
|
||||
if (!this.webSocketServers.length) {
|
||||
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,
|
||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[],
|
||||
newTransactions: MempoolTransactionExtended[], recentlyDeletedTransactions: MempoolTransactionExtended[][], accelerationDelta: string[],
|
||||
candidates?: GbtCandidates): Promise<void> {
|
||||
if (!this.webSocketServers.length) {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
@ -538,6 +588,8 @@ class WebsocketHandler {
|
||||
|
||||
this.printLogs();
|
||||
|
||||
const deletedTransactions = recentlyDeletedTransactions.length ? recentlyDeletedTransactions[0] : [];
|
||||
|
||||
const transactionIds = (memPool.limitGBT && candidates) ? Object.keys(candidates?.txs || {}) : Object.keys(newMempool);
|
||||
let added = newTransactions;
|
||||
let removed = deletedTransactions;
|
||||
@ -556,9 +608,9 @@ class WebsocketHandler {
|
||||
const mBlockDeltas = mempoolBlocks.getMempoolBlockDeltas();
|
||||
const mempoolInfo = memPool.getMempoolInfo();
|
||||
const vBytesPerSecond = memPool.getVBytesPerSecond();
|
||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions);
|
||||
const rbfTransactions = Common.findRbfTransactions(newTransactions, recentlyDeletedTransactions.flat());
|
||||
const da = difficultyAdjustment.getDifficultyAdjustment();
|
||||
const accelerations = memPool.getAccelerations();
|
||||
const accelerations = accelerationApi.getAccelerations();
|
||||
memPool.handleRbfTransactions(rbfTransactions);
|
||||
const rbfChanges = rbfCache.getRbfChanges();
|
||||
let rbfReplacements;
|
||||
@ -587,7 +639,7 @@ class WebsocketHandler {
|
||||
const replacedTransactions: { replaced: string, by: TransactionExtended }[] = [];
|
||||
for (const tx of newTransactions) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@ -666,10 +718,13 @@ class WebsocketHandler {
|
||||
const addressCache = this.makeAddressCache(newTransactions);
|
||||
const removedAddressCache = this.makeAddressCache(deletedTransactions);
|
||||
|
||||
const websocketAccelerationDelta = accelerationApi.getAccelerationDelta(this.accelerations, accelerations);
|
||||
this.accelerations = accelerations;
|
||||
|
||||
// pre-compute acceleration delta
|
||||
const accelerationUpdate = {
|
||||
added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||
removed: accelerationDelta.filter(txid => !accelerations[txid]),
|
||||
added: websocketAccelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null),
|
||||
removed: websocketAccelerationDelta.filter(txid => !accelerations[txid]),
|
||||
};
|
||||
|
||||
// TODO - Fix indentation after PR is merged
|
||||
@ -832,6 +887,7 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
},
|
||||
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
|
||||
};
|
||||
@ -873,6 +929,7 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
};
|
||||
if (!mempoolTx.cpfpChecked) {
|
||||
calculateMempoolTxCpfp(mempoolTx, newMempool);
|
||||
@ -940,6 +997,8 @@ class WebsocketHandler {
|
||||
throw new Error('No WebSocket.Server have been set');
|
||||
}
|
||||
|
||||
const blockTransactions = structuredClone(transactions);
|
||||
|
||||
this.printLogs();
|
||||
await statistics.runStatistics();
|
||||
|
||||
@ -949,10 +1008,10 @@ class WebsocketHandler {
|
||||
let transactionIds: string[] = (memPool.limitGBT) ? Object.keys(candidates?.txs || {}) : Object.keys(_memPool);
|
||||
|
||||
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());
|
||||
memPool.handleMinedRbfTransactions(rbfTransactions);
|
||||
memPool.handleRbfTransactions(rbfTransactions);
|
||||
memPool.removeFromSpendMap(transactions);
|
||||
|
||||
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
||||
@ -969,7 +1028,7 @@ class WebsocketHandler {
|
||||
}
|
||||
|
||||
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 stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
|
||||
@ -991,9 +1050,11 @@ class WebsocketHandler {
|
||||
});
|
||||
|
||||
BlocksAuditsRepository.$saveAudit({
|
||||
version: 1,
|
||||
time: block.timestamp,
|
||||
height: block.height,
|
||||
hash: block.id,
|
||||
unseenTxs: unseen,
|
||||
addedTxs: added,
|
||||
prioritizedTxs: prioritized,
|
||||
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 } = {};
|
||||
|
||||
// Update mempool to remove transactions included in the new block
|
||||
@ -1150,6 +1219,7 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
},
|
||||
accelerationPositions: memPool.getAccelerationPositions(mempoolTx.txid),
|
||||
});
|
||||
@ -1172,6 +1242,7 @@ class WebsocketHandler {
|
||||
accelerated: mempoolTx.acceleration || undefined,
|
||||
acceleratedBy: mempoolTx.acceleratedBy || undefined,
|
||||
acceleratedAt: mempoolTx.acceleratedAt || undefined,
|
||||
feeDelta: mempoolTx.feeDelta || undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ interface IConfig {
|
||||
AUTOMATIC_POOLS_UPDATE: boolean;
|
||||
POOLS_JSON_URL: string,
|
||||
POOLS_JSON_TREE_URL: string,
|
||||
POOLS_UPDATE_DELAY: number,
|
||||
AUDIT: boolean;
|
||||
RUST_GBT: boolean;
|
||||
LIMIT_GBT: boolean;
|
||||
@ -85,6 +86,7 @@ interface IConfig {
|
||||
TIMEOUT: number;
|
||||
COOKIE: boolean;
|
||||
COOKIE_PATH: string;
|
||||
DEBUG_LOG_PATH: string;
|
||||
};
|
||||
SECOND_CORE_RPC: {
|
||||
HOST: string;
|
||||
@ -200,8 +202,9 @@ const defaults: IConfig = {
|
||||
'AUTOMATIC_POOLS_UPDATE': false,
|
||||
'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_UPDATE_DELAY': 604800, // in seconds, default is one week
|
||||
'AUDIT': false,
|
||||
'RUST_GBT': false,
|
||||
'RUST_GBT': true,
|
||||
'LIMIT_GBT': false,
|
||||
'CPFP_INDEXING': false,
|
||||
'MAX_BLOCKS_BULK_QUERY': 0,
|
||||
@ -233,7 +236,8 @@ const defaults: IConfig = {
|
||||
'PASSWORD': 'mempool',
|
||||
'TIMEOUT': 60000,
|
||||
'COOKIE': false,
|
||||
'COOKIE_PATH': '/bitcoin/.cookie'
|
||||
'COOKIE_PATH': '/bitcoin/.cookie',
|
||||
'DEBUG_LOG_PATH': '',
|
||||
},
|
||||
'SECOND_CORE_RPC': {
|
||||
'HOST': '127.0.0.1',
|
||||
|
@ -213,6 +213,8 @@ class Server {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
poolsUpdater.$startService();
|
||||
}
|
||||
|
||||
async runMainUpdateLoop(): Promise<void> {
|
||||
@ -231,11 +233,11 @@ class Server {
|
||||
const newMempool = await bitcoinApi.$getRawMempool();
|
||||
const minFeeMempool = memPool.limitGBT ? await bitcoinSecondClient.getRawMemPool() : null;
|
||||
const minFeeTip = memPool.limitGBT ? await bitcoinSecondClient.getBlockCount() : -1;
|
||||
const newAccelerations = await accelerationApi.$updateAccelerations();
|
||||
const latestAccelerations = await accelerationApi.$updateAccelerations();
|
||||
const numHandledBlocks = await blocks.$updateBlocks();
|
||||
const pollRate = config.MEMPOOL.POLL_RATE_MS * (indexer.indexerIsRunning() ? 10 : 1);
|
||||
if (numHandledBlocks === 0) {
|
||||
await memPool.$updateMempool(newMempool, newAccelerations, minFeeMempool, minFeeTip, pollRate);
|
||||
await memPool.$updateMempool(newMempool, latestAccelerations, minFeeMempool, minFeeTip, pollRate);
|
||||
}
|
||||
indexer.$run();
|
||||
if (config.WALLETS.ENABLED) {
|
||||
@ -316,8 +318,10 @@ class Server {
|
||||
priceUpdater.setRatesChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
|
||||
}
|
||||
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
|
||||
|
||||
accelerationApi.connectWebsocket();
|
||||
}
|
||||
|
||||
|
||||
setUpHttpApiRoutes(): void {
|
||||
bitcoinRoutes.initRoutes(this.app);
|
||||
bitcoinCoreRoutes.initRoutes(this.app);
|
||||
|
@ -10,6 +10,7 @@ import config from './config';
|
||||
import auditReplicator from './replication/AuditReplication';
|
||||
import statisticsReplicator from './replication/StatisticsReplication';
|
||||
import AccelerationRepository from './repositories/AccelerationRepository';
|
||||
import BlocksAuditsRepository from './repositories/BlocksAuditsRepository';
|
||||
|
||||
export interface CoreIndex {
|
||||
name: string;
|
||||
@ -192,6 +193,7 @@ class Indexer {
|
||||
await auditReplicator.$sync();
|
||||
await statisticsReplicator.$sync();
|
||||
await AccelerationRepository.$indexPastAccelerations();
|
||||
await BlocksAuditsRepository.$migrateAuditsV0toV1();
|
||||
// do not wait for classify blocks to finish
|
||||
blocks.$classifyBlocks();
|
||||
} catch (e) {
|
||||
|
@ -29,9 +29,11 @@ export interface PoolStats extends PoolInfo {
|
||||
}
|
||||
|
||||
export interface BlockAudit {
|
||||
version: number,
|
||||
time: number,
|
||||
height: number,
|
||||
hash: string,
|
||||
unseenTxs: string[],
|
||||
missingTxs: string[],
|
||||
freshTxs: string[],
|
||||
sigopTxs: string[],
|
||||
@ -126,6 +128,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
||||
acceleration?: boolean;
|
||||
acceleratedBy?: number[];
|
||||
acceleratedAt?: number;
|
||||
feeDelta?: number;
|
||||
replacement?: boolean;
|
||||
uid?: 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`
|
||||
name: string;
|
||||
slug: string;
|
||||
minerNames: string[] | null;
|
||||
};
|
||||
avgFee: number;
|
||||
avgFeeRate: number;
|
||||
@ -316,6 +320,7 @@ export interface BlockExtension {
|
||||
segwitTotalSize: number;
|
||||
segwitTotalWeight: number;
|
||||
header: string;
|
||||
firstSeen: number | null;
|
||||
utxoSetChange: number;
|
||||
// Requires coinstatsindex, will be set to NULL otherwise
|
||||
utxoSetSize: number | null;
|
||||
@ -382,8 +387,9 @@ export interface CpfpCluster {
|
||||
}
|
||||
|
||||
export interface CpfpSummary {
|
||||
transactions: TransactionExtended[];
|
||||
transactions: MempoolTransactionExtended[];
|
||||
clusters: CpfpCluster[];
|
||||
version: number;
|
||||
}
|
||||
|
||||
export interface Statistic {
|
||||
@ -449,7 +455,7 @@ export interface OptimizedStatistic {
|
||||
|
||||
export interface TxTrackingInfo {
|
||||
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?: {
|
||||
ancestors?: Ancestor[],
|
||||
bestDescendant?: Ancestor | null,
|
||||
@ -462,6 +468,7 @@ export interface TxTrackingInfo {
|
||||
accelerated?: boolean,
|
||||
acceleratedBy?: number[],
|
||||
acceleratedAt?: number,
|
||||
feeDelta?: number,
|
||||
confirmed?: boolean
|
||||
}
|
||||
|
||||
|
@ -31,11 +31,11 @@ class AuditReplication {
|
||||
const missingAudits = await this.$getMissingAuditBlocks();
|
||||
|
||||
logger.debug(`Fetching missing audit data for ${missingAudits.length} blocks from trusted servers`, 'Replication');
|
||||
|
||||
|
||||
let totalSynced = 0;
|
||||
let totalMissed = 0;
|
||||
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) {
|
||||
const slice = missingAudits.slice(i, i + BATCH_SIZE);
|
||||
const results = await Promise.all(slice.map(hash => this.$syncAudit(hash)));
|
||||
@ -109,9 +109,11 @@ class AuditReplication {
|
||||
version: 1,
|
||||
});
|
||||
await blocksAuditsRepository.$saveAudit({
|
||||
version: auditSummary.version || 0,
|
||||
hash: blockHash,
|
||||
height: auditSummary.height,
|
||||
time: auditSummary.timestamp || auditSummary.time,
|
||||
unseenTxs: auditSummary.unseenTxs || [],
|
||||
missingTxs: auditSummary.missingTxs || [],
|
||||
addedTxs: auditSummary.addedTxs || [],
|
||||
prioritizedTxs: auditSummary.prioritizedTxs || [],
|
||||
|
@ -192,6 +192,7 @@ class AccelerationRepository {
|
||||
}
|
||||
}
|
||||
|
||||
// modifies block transactions
|
||||
public async $indexAccelerationsForBlock(block: BlockExtended, accelerations: Acceleration[], transactions: MempoolTransactionExtended[]): Promise<void> {
|
||||
const blockTxs: { [txid: string]: MempoolTransactionExtended } = {};
|
||||
for (const tx of transactions) {
|
||||
|
@ -1,13 +1,24 @@
|
||||
import blocks from '../api/blocks';
|
||||
import DB from '../database';
|
||||
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 {
|
||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||
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)
|
||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
||||
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.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]);
|
||||
} catch (e: any) {
|
||||
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> {
|
||||
try {
|
||||
const [rows]: any[] = await DB.query(
|
||||
`SELECT blocks_audits.height, blocks_audits.hash as id, UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
|
||||
template,
|
||||
missing_txs as missingTxs,
|
||||
added_txs as addedTxs,
|
||||
prioritized_txs as prioritizedTxs,
|
||||
fresh_txs as freshTxs,
|
||||
sigop_txs as sigopTxs,
|
||||
fullrbf_txs as fullrbfTxs,
|
||||
accelerated_txs as acceleratedTxs,
|
||||
match_rate as matchRate,
|
||||
expected_fees as expectedFees,
|
||||
expected_weight as expectedWeight
|
||||
`SELECT
|
||||
blocks_audits.version,
|
||||
blocks_audits.height,
|
||||
blocks_audits.hash as id,
|
||||
UNIX_TIMESTAMP(blocks_audits.time) as timestamp,
|
||||
template,
|
||||
unseen_txs as unseenTxs,
|
||||
missing_txs as missingTxs,
|
||||
added_txs as addedTxs,
|
||||
prioritized_txs as prioritizedTxs,
|
||||
fresh_txs as freshTxs,
|
||||
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
|
||||
JOIN blocks_templates ON blocks_templates.id = blocks_audits.hash
|
||||
WHERE blocks_audits.hash = ?
|
||||
`, [hash]);
|
||||
|
||||
if (rows.length) {
|
||||
rows[0].unseenTxs = JSON.parse(rows[0].unseenTxs);
|
||||
rows[0].missingTxs = JSON.parse(rows[0].missingTxs);
|
||||
rows[0].addedTxs = JSON.parse(rows[0].addedTxs);
|
||||
rows[0].prioritizedTxs = JSON.parse(rows[0].prioritizedTxs);
|
||||
@ -101,7 +118,7 @@ class BlocksAuditRepositories {
|
||||
public async $getBlockTxAudit(hash: string, txid: string): Promise<TransactionAudit | null> {
|
||||
try {
|
||||
const blockAudit = await this.$getBlockAudit(hash);
|
||||
|
||||
|
||||
if (blockAudit) {
|
||||
const isAdded = blockAudit.addedTxs.includes(txid);
|
||||
const isPrioritized = blockAudit.prioritizedTxs.includes(txid);
|
||||
@ -115,16 +132,17 @@ class BlocksAuditRepositories {
|
||||
firstSeen = tx.time;
|
||||
}
|
||||
});
|
||||
const wasSeen = blockAudit.version === 1 ? !blockAudit.unseenTxs.includes(txid) : (isExpected || isPrioritized || isAccelerated);
|
||||
|
||||
return {
|
||||
seen: isExpected || isPrioritized || isAccelerated,
|
||||
seen: wasSeen,
|
||||
expected: isExpected,
|
||||
added: isAdded,
|
||||
added: isAdded && (blockAudit.version === 0 || !wasSeen),
|
||||
prioritized: isPrioritized,
|
||||
conflict: isConflict,
|
||||
accelerated: isAccelerated,
|
||||
firstSeen,
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch (e: any) {
|
||||
@ -186,6 +204,96 @@ class BlocksAuditRepositories {
|
||||
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();
|
||||
|
@ -14,6 +14,7 @@ import chainTips from '../api/chain-tips';
|
||||
import blocks from '../api/blocks';
|
||||
import BlocksAuditsRepository from './BlocksAuditsRepository';
|
||||
import transactionUtils from '../api/transaction-utils';
|
||||
import { parseDATUMTemplateCreator } from '../utils/bitcoin-script';
|
||||
|
||||
interface DatabaseBlock {
|
||||
id: string;
|
||||
@ -56,6 +57,7 @@ interface DatabaseBlock {
|
||||
utxoSetChange: number;
|
||||
utxoSetSize: number;
|
||||
totalInputAmt: number;
|
||||
firstSeen: number;
|
||||
}
|
||||
|
||||
const BLOCK_DB_FIELDS = `
|
||||
@ -98,7 +100,8 @@ const BLOCK_DB_FIELDS = `
|
||||
blocks.header,
|
||||
blocks.utxoset_change AS utxoSetChange,
|
||||
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 {
|
||||
@ -498,7 +501,7 @@ class BlocksRepository {
|
||||
}
|
||||
|
||||
query += ` ORDER BY height DESC
|
||||
LIMIT 10`;
|
||||
LIMIT 100`;
|
||||
|
||||
try {
|
||||
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
|
||||
* must provide the correct field into dbBlk object param
|
||||
@ -1054,6 +1075,7 @@ class BlocksRepository {
|
||||
id: dbBlk.poolId,
|
||||
name: dbBlk.poolName,
|
||||
slug: dbBlk.poolSlug,
|
||||
minerNames: null,
|
||||
};
|
||||
extras.avgFee = dbBlk.avgFee;
|
||||
extras.avgFeeRate = dbBlk.avgFeeRate;
|
||||
@ -1076,6 +1098,7 @@ class BlocksRepository {
|
||||
extras.utxoSetSize = dbBlk.utxoSetSize;
|
||||
extras.totalInputAmt = dbBlk.totalInputAmt;
|
||||
extras.virtualSize = dbBlk.weight / 4.0;
|
||||
extras.firstSeen = dbBlk.firstSeen;
|
||||
|
||||
// Re-org can happen after indexing so we need to always get the
|
||||
// latest state from core
|
||||
@ -1106,7 +1129,7 @@ class BlocksRepository {
|
||||
let summaryVersion = 0;
|
||||
if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||
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;
|
||||
} else {
|
||||
// Call Core RPC
|
||||
@ -1123,6 +1146,10 @@ class BlocksRepository {
|
||||
}
|
||||
}
|
||||
|
||||
if (extras.pool.name === 'OCEAN') {
|
||||
extras.pool.minerNames = parseDATUMTemplateCreator(extras.coinbaseRaw);
|
||||
}
|
||||
|
||||
blk.extras = <BlockExtension>extras;
|
||||
return <BlockExtended>blk;
|
||||
}
|
||||
|
@ -83,6 +83,7 @@ module.exports = {
|
||||
signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+
|
||||
stop: 'stop',
|
||||
submitBlock: 'submitblock', // bitcoind v0.7.0+
|
||||
submitPackage: 'submitpackage',
|
||||
validateAddress: 'validateaddress',
|
||||
verifyChain: 'verifychain', // bitcoind v0.9.0+
|
||||
verifyMessage: 'verifymessage',
|
||||
|
@ -6,16 +6,30 @@ import backendInfo from '../api/backend-info';
|
||||
import logger from '../logger';
|
||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||
import * as https from 'https';
|
||||
import { Common } from '../api/common';
|
||||
|
||||
/**
|
||||
* Maintain the most recent version of pools-v2.json
|
||||
*/
|
||||
class PoolsUpdater {
|
||||
tag = 'PoolsUpdater';
|
||||
|
||||
lastRun: number = 0;
|
||||
currentSha: string | null = null;
|
||||
poolsUrl: string = config.MEMPOOL.POOLS_JSON_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> {
|
||||
if (['mainnet', 'testnet', 'signet'].includes(config.MEMPOOL.NETWORK) === false ||
|
||||
config.MEMPOOL.ENABLED === false
|
||||
@ -23,11 +37,8 @@ class PoolsUpdater {
|
||||
return;
|
||||
}
|
||||
|
||||
const oneWeek = 604800;
|
||||
const oneDay = 86400;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -43,7 +54,7 @@ class PoolsUpdater {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@ -53,16 +64,16 @@ class PoolsUpdater {
|
||||
config.MEMPOOL.AUTOMATIC_POOLS_UPDATE !== true && // Automatic pools update is disabled
|
||||
!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.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.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`, this.tag);
|
||||
return;
|
||||
}
|
||||
|
||||
const network = config.SOCKS5PROXY.ENABLED ? 'tor' : 'clearnet';
|
||||
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 {
|
||||
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);
|
||||
if (poolsJson === undefined) {
|
||||
@ -71,7 +82,7 @@ class PoolsUpdater {
|
||||
poolsParser.setMiningPools(poolsJson);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -81,14 +92,14 @@ class PoolsUpdater {
|
||||
await this.updateDBSha(githubSha);
|
||||
await DB.query('COMMIT;');
|
||||
} 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;');
|
||||
}
|
||||
logger.info(`Mining pools-v2.json (${githubSha}) import completed`);
|
||||
logger.info(`Mining pools-v2.json (${githubSha}) import completed`, this.tag);
|
||||
|
||||
} catch (e) {
|
||||
this.lastRun = now - (oneWeek - oneDay); // Try again in 24h instead of waiting next week
|
||||
logger.err(`PoolsUpdater failed. Will try again in 24h. Exception: ${JSON.stringify(e)}`, logger.tags.mining);
|
||||
this.lastRun = now - 600; // Try again in 10 minutes
|
||||
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(`INSERT INTO state VALUES('pools_json_sha', NULL, '${githubSha}')`);
|
||||
} 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"');
|
||||
return (rows.length > 0 ? rows[0].string : null);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -186,7 +197,7 @@ class PoolsUpdater {
|
||||
}
|
||||
return data.data;
|
||||
} 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++;
|
||||
}
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)?.[0] || '', 10);
|
||||
@ -200,4 +200,28 @@ export function getVarIntLength(n: number): number {
|
||||
} else {
|
||||
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,
|
||||
"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_UPDATE_DELAY": 604800,
|
||||
"CPFP_INDEXING": false,
|
||||
"MAX_BLOCKS_BULK_QUERY": 0,
|
||||
"DISK_CACHE_BLOCK_INTERVAL": 6,
|
||||
@ -140,6 +141,7 @@ Corresponding `docker-compose.yml` overrides:
|
||||
MEMPOOL_AUTOMATIC_POOLS_UPDATE: ""
|
||||
MEMPOOL_POOLS_JSON_URL: ""
|
||||
MEMPOOL_POOLS_JSON_TREE_URL: ""
|
||||
MEMPOOL_POOLS_UPDATE_DELAY: ""
|
||||
MEMPOOL_CPFP_INDEXING: ""
|
||||
MEMPOOL_MAX_BLOCKS_BULK_QUERY: ""
|
||||
MEMPOOL_DISK_CACHE_BLOCK_INTERVAL: ""
|
||||
|
@ -36,6 +36,7 @@
|
||||
"ALLOW_UNREACHABLE": __MEMPOOL_ALLOW_UNREACHABLE__,
|
||||
"POOLS_JSON_TREE_URL": "__MEMPOOL_POOLS_JSON_TREE_URL__",
|
||||
"POOLS_JSON_URL": "__MEMPOOL_POOLS_JSON_URL__",
|
||||
"POOLS_UPDATE_DELAY": __MEMPOOL_POOLS_UPDATE_DELAY__,
|
||||
"PRICE_UPDATES_PER_HOUR": __MEMPOOL_PRICE_UPDATES_PER_HOUR__,
|
||||
"MAX_TRACKED_ADDRESSES": __MEMPOOL_MAX_TRACKED_ADDRESSES__
|
||||
},
|
||||
@ -46,7 +47,8 @@
|
||||
"PASSWORD": "__CORE_RPC_PASSWORD__",
|
||||
"TIMEOUT": __CORE_RPC_TIMEOUT__,
|
||||
"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": {
|
||||
"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_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_UPDATE_DELAY__=${MEMPOOL_POOLS_UPDATE_DELAY:=604800}
|
||||
__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_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
|
||||
__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_COOKIE__=${CORE_RPC_COOKIE:=false}
|
||||
__CORE_RPC_COOKIE_PATH__=${CORE_RPC_COOKIE_PATH:=""}
|
||||
__CORE_RPC_DEBUG_LOG_PATH__=${CORE_RPC_DEBUG_LOG_PATH:=""}
|
||||
|
||||
# ELECTRUM
|
||||
__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:=[]}
|
||||
|
||||
# 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}
|
||||
|
||||
# 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_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_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_RUST_GBT__!${__MEMPOOL_RUST_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_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_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_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}
|
||||
__ACCELERATOR__=${ACCELERATOR:=false}
|
||||
__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}
|
||||
__HISTORICAL_PRICE__=${HISTORICAL_PRICE:=true}
|
||||
__ADDITIONAL_CURRENCIES__=${ADDITIONAL_CURRENCIES:=false}
|
||||
|
@ -33,7 +33,7 @@ $ npm run config:defaults:liquid
|
||||
|
||||
### 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:
|
||||
|
||||
@ -70,7 +70,7 @@ Set up the [Mempool backend](../backend/) first, if you haven't already.
|
||||
|
||||
### 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:
|
||||
|
||||
|
@ -54,6 +54,10 @@
|
||||
"translation": "src/locale/messages.fr.xlf",
|
||||
"baseHref": "/fr/"
|
||||
},
|
||||
"hr": {
|
||||
"translation": "src/locale/messages.hr.xlf",
|
||||
"baseHref": "/hr/"
|
||||
},
|
||||
"ja": {
|
||||
"translation": "src/locale/messages.ja.xlf",
|
||||
"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').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;
|
||||
});
|
||||
});
|
||||
cy.get('.alert-replaced').should('be.visible');
|
||||
});
|
||||
|
||||
it('shows RBF transactions properly (desktop)', () => {
|
||||
|
@ -750,7 +750,7 @@
|
||||
},
|
||||
"backendInfo": {
|
||||
"hostname": "node205.tk7.mempool.space",
|
||||
"version": "3.0.0-dev",
|
||||
"version": "3.1.0-dev",
|
||||
"gitCommit": "abbc8a134",
|
||||
"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",
|
||||
"version": "3.0.0-dev",
|
||||
"version": "3.1.0-dev",
|
||||
"description": "Bitcoin mempool visualizer and blockchain explorer backend",
|
||||
"license": "GNU Affero General Public License v3.0",
|
||||
"homepage": "https://mempool.space",
|
||||
@ -92,10 +92,10 @@
|
||||
"ngx-infinite-scroll": "^17.0.0",
|
||||
"qrcode": "1.5.1",
|
||||
"rxjs": "~7.8.1",
|
||||
"esbuild": "^0.23.0",
|
||||
"esbuild": "^0.24.0",
|
||||
"tinyify": "^4.0.0",
|
||||
"tlite": "^0.1.9",
|
||||
"tslib": "~2.6.0",
|
||||
"tslib": "~2.8.0",
|
||||
"zone.js": "~0.14.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -105,7 +105,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
||||
"@typescript-eslint/parser": "^7.4.0",
|
||||
"eslint": "^8.57.0",
|
||||
"browser-sync": "^3.0.0",
|
||||
"browser-sync": "^3.0.3",
|
||||
"http-proxy-middleware": "~2.0.6",
|
||||
"prettier": "^3.0.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
@ -115,7 +115,7 @@
|
||||
"optionalDependencies": {
|
||||
"@cypress/schematic": "^2.5.0",
|
||||
"@types/cypress": "^1.1.3",
|
||||
"cypress": "^13.13.0",
|
||||
"cypress": "^13.15.0",
|
||||
"cypress-fail-on-console-error": "~5.1.0",
|
||||
"cypress-wait-until": "^2.0.1",
|
||||
"mock-socket": "~9.3.1",
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AppPreloadingStrategy } from './app.preloading-strategy'
|
||||
import { BlockViewComponent } from './components/block-view/block-view.component';
|
||||
import { EightBlocksComponent } from './components/eight-blocks/eight-blocks.component';
|
||||
import { MempoolBlockViewComponent } from './components/mempool-block-view/mempool-block-view.component';
|
||||
import { ClockComponent } from './components/clock/clock.component';
|
||||
import { StatusViewComponent } from './components/status-view/status-view.component';
|
||||
import { AddressGroupComponent } from './components/address-group/address-group.component';
|
||||
import { TrackerComponent } from './components/tracker/tracker.component';
|
||||
import { AccelerateCheckout } from './components/accelerate-checkout/accelerate-checkout.component';
|
||||
import { AppPreloadingStrategy } from '@app/app.preloading-strategy'
|
||||
import { BlockViewComponent } from '@components/block-view/block-view.component';
|
||||
import { EightBlocksComponent } from '@components/eight-blocks/eight-blocks.component';
|
||||
import { MempoolBlockViewComponent } from '@components/mempool-block-view/mempool-block-view.component';
|
||||
import { ClockComponent } from '@components/clock/clock.component';
|
||||
import { StatusViewComponent } from '@components/status-view/status-view.component';
|
||||
import { AddressGroupComponent } from '@components/address-group/address-group.component';
|
||||
import { TrackerComponent } from '@components/tracker/tracker.component';
|
||||
import { AccelerateCheckout } from '@components/accelerate-checkout/accelerate-checkout.component';
|
||||
import { TrackerGuard } from '@app/route-guards';
|
||||
|
||||
const browserWindow = window || {};
|
||||
// @ts-ignore
|
||||
@ -21,16 +22,16 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
path: 'widget/wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
@ -44,7 +45,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
@ -59,12 +60,12 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
@ -82,7 +83,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
@ -102,16 +103,16 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
path: 'widget/wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
@ -125,7 +126,7 @@ let routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
@ -137,21 +138,22 @@ let routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'tx',
|
||||
canMatch: [TrackerGuard],
|
||||
runGuardsAndResolvers: 'always',
|
||||
loadChildren: () => import('@components/tracker/tracker.module').then(m => m.TrackerModule),
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule),
|
||||
loadChildren: () => import('@app/master-page.module').then(m => m.MasterPageModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: 'tracker',
|
||||
data: { networkSpecific: true },
|
||||
loadChildren: () => import('./components/tracker/tracker.module').then(m => m.TrackerModule),
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
path: 'widget/wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
@ -163,19 +165,19 @@ let routes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'testnet',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
path: 'testnet4',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
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: '',
|
||||
loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
loadChildren: () => import('@app/bitcoin-graphs.module').then(m => m.BitcoinGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
},
|
||||
];
|
||||
|
||||
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
@ -227,16 +225,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
{
|
||||
path: '',
|
||||
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 },
|
||||
},
|
||||
{
|
||||
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 },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
path: 'widget/wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
@ -250,7 +248,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
},
|
||||
{
|
||||
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 },
|
||||
},
|
||||
{
|
||||
@ -262,16 +260,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
{
|
||||
path: '',
|
||||
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 },
|
||||
},
|
||||
{
|
||||
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 },
|
||||
},
|
||||
{
|
||||
path: 'wallet',
|
||||
path: 'widget/wallet',
|
||||
children: [],
|
||||
component: AddressGroupComponent,
|
||||
data: {
|
||||
@ -283,11 +281,11 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./previews.module').then(m => m.PreviewsModule)
|
||||
loadChildren: () => import('@app/previews.module').then(m => m.PreviewsModule)
|
||||
},
|
||||
{
|
||||
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: '',
|
||||
loadChildren: () => import('./liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
loadChildren: () => import('@app/liquid/liquid-graphs.module').then(m => m.LiquidGraphsModule),
|
||||
data: { preload: true },
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (!window['isMempoolSpaceBuild']) {
|
||||
routes.push({
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
});
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes, {
|
||||
initialNavigation: 'enabledBlocking',
|
||||
|
@ -151,7 +151,7 @@ export const languages: Language[] = [
|
||||
{ code: 'fr', name: 'Français' }, // French
|
||||
// { code: 'gl', name: 'Galego' }, // Galician
|
||||
{ code: 'ko', name: '한국어' }, // Korean
|
||||
// { code: 'hr', name: 'Hrvatski' }, // Croatian
|
||||
{ code: 'hr', name: 'Hrvatski' }, // Croatian
|
||||
// { code: 'id', name: 'Bahasa Indonesia' },// Indonesian
|
||||
{ code: 'hi', name: 'हिन्दी' }, // Hindi
|
||||
{ code: 'ne', name: 'नेपाली' }, // Nepalese
|
||||
|
@ -2,11 +2,11 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
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 { AppComponent } from './components/app/app.component';
|
||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
import { ZoneService } from './services/zone.service';
|
||||
import { AppComponent } from '@components/app/app.component';
|
||||
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
|
||||
import { ZoneService } from '@app/services/zone.service';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@ -20,4 +20,4 @@ import { ZoneService } from './services/zone.service';
|
||||
],
|
||||
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 { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
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 { AppComponent } from './components/app/app.component';
|
||||
import { ElectrsApiService } from './services/electrs-api.service';
|
||||
import { StateService } from './services/state.service';
|
||||
import { CacheService } from './services/cache.service';
|
||||
import { PriceService } from './services/price.service';
|
||||
import { EnterpriseService } from './services/enterprise.service';
|
||||
import { WebsocketService } from './services/websocket.service';
|
||||
import { AudioService } from './services/audio.service';
|
||||
import { PreloadService } from './services/preload.service';
|
||||
import { SeoService } from './services/seo.service';
|
||||
import { OpenGraphService } from './services/opengraph.service';
|
||||
import { ZoneService } from './services/zone-shim.service';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { StorageService } from './services/storage.service';
|
||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
import { LanguageService } from './services/language.service';
|
||||
import { ThemeService } from './services/theme.service';
|
||||
import { FiatShortenerPipe } from './shared/pipes/fiat-shortener.pipe';
|
||||
import { FiatCurrencyPipe } from './shared/pipes/fiat-currency.pipe';
|
||||
import { ShortenStringPipe } from './shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||
import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe';
|
||||
import { AppPreloadingStrategy } from './app.preloading-strategy';
|
||||
import { ServicesApiServices } from './services/services-api.service';
|
||||
import { AppComponent } from '@components/app/app.component';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
import { OrdApiService } from '@app/services/ord-api.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { CacheService } from '@app/services/cache.service';
|
||||
import { PriceService } from '@app/services/price.service';
|
||||
import { EnterpriseService } from '@app/services/enterprise.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { PreloadService } from '@app/services/preload.service';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||
import { ZoneService } from '@app/services/zone-shim.service';
|
||||
import { SharedModule } from '@app/shared/shared.module';
|
||||
import { StorageService } from '@app/services/storage.service';
|
||||
import { HttpCacheInterceptor } from '@app/services/http-cache.interceptor';
|
||||
import { LanguageService } from '@app/services/language.service';
|
||||
import { ThemeService } from '@app/services/theme.service';
|
||||
import { TimeService } from '@app/services/time.service';
|
||||
import { FiatShortenerPipe } from '@app/shared/pipes/fiat-shortener.pipe';
|
||||
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
|
||||
import { ShortenStringPipe } from '@app/shared/pipes/shorten-string-pipe/shorten-string.pipe';
|
||||
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';
|
||||
|
||||
const providers = [
|
||||
ElectrsApiService,
|
||||
OrdApiService,
|
||||
StateService,
|
||||
CacheService,
|
||||
PriceService,
|
||||
@ -42,6 +45,7 @@ const providers = [
|
||||
EnterpriseService,
|
||||
LanguageService,
|
||||
ThemeService,
|
||||
TimeService,
|
||||
ShortenStringPipe,
|
||||
FiatShortenerPipe,
|
||||
FiatCurrencyPipe,
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
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 = [
|
||||
{
|
||||
path: '',
|
||||
component: MasterPageComponent,
|
||||
loadChildren: () => import('./graphs/graphs.module').then(m => m.GraphsModule),
|
||||
loadChildren: () => import('@app/graphs/graphs.module').then(m => m.GraphsModule),
|
||||
data: { preload: true },
|
||||
}
|
||||
];
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Transaction, Vin } from './interfaces/electrs.interface';
|
||||
import { Hash } from './shared/sha256';
|
||||
import { Transaction, Vin } from '@interfaces/electrs.interface';
|
||||
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_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;
|
||||
}
|
||||
const opN = ops.pop();
|
||||
if (!opN.startsWith('OP_PUSHNUM_')) {
|
||||
if (opN !== 'OP_0' && !opN.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
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();
|
||||
if (!opM.startsWith('OP_PUSHNUM_')) {
|
||||
if (opM !== 'OP_0' && !opM.startsWith('OP_PUSHNUM_')) {
|
||||
return;
|
||||
}
|
||||
const m = parseInt(opM.match(/[0-9]+/)[0], 10);
|
||||
@ -303,4 +303,4 @@ export async function calcScriptHash$(script: string): Promise<string> {
|
||||
return hashArray
|
||||
.map((bytes) => bytes.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { EnterpriseService } from '@app/services/enterprise.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about-sponsors',
|
||||
|
@ -53,7 +53,7 @@
|
||||
<span>Spiral</span>
|
||||
</a>
|
||||
<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>
|
||||
<style>
|
||||
.d {
|
||||
@ -125,17 +125,14 @@
|
||||
<span>Blockstream</span>
|
||||
</a>
|
||||
<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>
|
||||
</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 href="https://bitkey.world/" target="_blank" title="Bitkey">
|
||||
<img class="image" src="/resources/profile/bitkey.svg" />
|
||||
<span>Bitkey</span>
|
||||
</a>
|
||||
<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">
|
||||
@ -150,7 +147,7 @@
|
||||
<span>Bull Bitcoin</span>
|
||||
</a>
|
||||
<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"/>
|
||||
<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)"/>
|
||||
@ -191,12 +188,30 @@
|
||||
</svg>
|
||||
<span>Exodus</span>
|
||||
</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>
|
||||
|
||||
<ng-container>
|
||||
<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>
|
||||
<div class="wrapper">
|
||||
<ng-container>
|
||||
@ -435,7 +450,7 @@
|
||||
Trademark Notice<br>
|
||||
</div>
|
||||
<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>
|
||||
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 {
|
||||
border-radius: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.intro {
|
||||
@ -94,6 +92,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.whale-sponsor {
|
||||
img {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.alliances {
|
||||
margin-bottom: 100px;
|
||||
a {
|
||||
@ -158,9 +163,8 @@
|
||||
margin: 40px 29px 10px;
|
||||
&.image.coldcard {
|
||||
border-radius: 0;
|
||||
width: auto;
|
||||
max-height: 50px;
|
||||
margin: 40px 29px 14px 29px;
|
||||
height: auto;
|
||||
margin: 20px 29px 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -254,3 +258,12 @@
|
||||
width: 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 { WebsocketService } from '../../services/websocket.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { IBackendInfo } from '../../interfaces/websocket.interface';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { IBackendInfo } from '@interfaces/websocket.interface';
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
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 { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { EnterpriseService } from '@app/services/enterprise.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { AboutComponent } from './about.component';
|
||||
import { AboutSponsorsComponent } from './about-sponsors.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { AboutComponent } from '@components/about/about.component';
|
||||
import { AboutSponsorsComponent } from '@components/about/about-sponsors.component';
|
||||
import { SharedModule } from '@app/shared/shared.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -389,21 +389,29 @@
|
||||
</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">
|
||||
<p class="text-nowrap">—<span i18n="or">OR</span>—</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (canPayWithCashapp || canPayWithApplePay) {
|
||||
@if (canPayWithCashapp || canPayWithApplePay || canPayWithGooglePay) {
|
||||
<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>
|
||||
@if (canPayWithCashapp) {
|
||||
<img class="paymentMethod mx-2" style="width: 200px" src="/resources/cash-app.svg" height=55 (click)="moveToStep('cashapp')">
|
||||
}
|
||||
@if (canPayWithApplePay) {
|
||||
@if (canPayWithCashapp) { <hr class="w-25 mt-2 mb-2"> }
|
||||
<img style="cursor: pointer;" src="/resources/apple-pay.svg" height=55 (click)="moveToStep('applepay')">
|
||||
@if (canPayWithCashapp) { <span class="mt-1 mb-1"></span> }
|
||||
<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>
|
||||
}
|
||||
@ -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>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (step === 'cashapp' || step === 'applepay') {
|
||||
} @else if (step === 'cashapp' || step === 'applepay' || step === 'googlepay') {
|
||||
<!-- Show checkout page -->
|
||||
<div class="row mb-md-1 text-center" id="confirm-title">
|
||||
<div class="col-sm" id="confirm-payment-title">
|
||||
@ -443,7 +451,7 @@
|
||||
</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="col-sm">
|
||||
<div class="form-group w-100">
|
||||
@ -463,11 +471,13 @@
|
||||
<div class="col-sm">
|
||||
<div class="form-group w-100">
|
||||
@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') {
|
||||
<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">
|
||||
<span i18n="accelerator.loading-payment-method">Loading payment method...</span>
|
||||
<div class="ml-2 spinner-border text-light" style="width: 25px; height: 25px"></div>
|
||||
@ -515,7 +525,7 @@
|
||||
<div class="col-sm">
|
||||
<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>
|
||||
@if (timeSincePaid > 20000) {
|
||||
@if (timeSincePaid > 30000) {
|
||||
<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>
|
||||
|
@ -172,10 +172,6 @@
|
||||
background-color: var(--tertiary);
|
||||
}
|
||||
|
||||
.btn-small-height {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
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 { Subscription, tap, of, catchError, Observable, switchMap } from 'rxjs';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
import { md5, nextRoundNumber, insecureRandomUUID } from '../../shared/common.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
import { ETA, EtaService } from '../../services/eta.service';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { MiningStats } from '../../services/mining.service';
|
||||
import { IAuth, AuthServiceMempool } from '../../services/auth.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { md5 } from '@app/shared/common.utils';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { ETA, EtaService } from '@app/services/eta.service';
|
||||
import { Transaction } from '@interfaces/electrs.interface';
|
||||
import { MiningStats } from '@app/services/mining.service';
|
||||
import { IAuth, AuthServiceMempool } from '@app/services/auth.service';
|
||||
import { EnterpriseService } from '@app/services/enterprise.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 = {
|
||||
hasAccess: boolean;
|
||||
@ -24,7 +26,7 @@ export type AccelerationEstimate = {
|
||||
mempoolBaseFee: number;
|
||||
vsizeFee: number;
|
||||
pools: number[];
|
||||
availablePaymentMethods: {[method: string]: {min: number, max: number}};
|
||||
availablePaymentMethods: Record<PaymentMethod, {min: number, max: number}>;
|
||||
unavailable?: boolean;
|
||||
options: { // recommended bid options
|
||||
fee: number; // recommended userBid in sats
|
||||
@ -47,7 +49,7 @@ export const MIN_BID_RATIO = 1;
|
||||
export const DEFAULT_BID_RATIO = 2;
|
||||
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({
|
||||
selector: 'app-accelerate-checkout',
|
||||
@ -62,6 +64,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
@Input() scrollEvent: boolean;
|
||||
@Input() cashappEnabled: boolean = true;
|
||||
@Input() applePayEnabled: boolean = false;
|
||||
@Input() googlePayEnabled: boolean = true;
|
||||
@Input() advancedEnabled: boolean = false;
|
||||
@Input() forceMobile: boolean = false;
|
||||
@Input() showDetails: boolean = false;
|
||||
@ -72,6 +75,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
@Output() changeMode = new EventEmitter<boolean>();
|
||||
|
||||
calculating = true;
|
||||
processing = false;
|
||||
selectedOption: 'wait' | 'accel';
|
||||
cantPayReason = '';
|
||||
quoteError = ''; // error fetching estimate or initial data
|
||||
@ -80,18 +84,16 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
timePaid: number = 0; // time acceleration requested
|
||||
math = Math;
|
||||
isMobile: boolean = window.innerWidth <= 767.98;
|
||||
isProdDomain = false;
|
||||
|
||||
private _step: CheckoutStep = 'summary';
|
||||
simpleMode: boolean = true;
|
||||
paymentMethod: 'cashapp' | 'btcpay';
|
||||
timeoutTimer: any;
|
||||
|
||||
authSubscription$: Subscription;
|
||||
auth: IAuth | null = null;
|
||||
|
||||
// accelerator stuff
|
||||
square: { appId: string, locationId: string};
|
||||
accelerationUUID: string;
|
||||
accelerationSubscription: Subscription;
|
||||
difficultySubscription: Subscription;
|
||||
estimateSubscription: Subscription;
|
||||
@ -112,14 +114,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
// square
|
||||
loadingCashapp = false;
|
||||
loadingApplePay = false;
|
||||
cashappError = false;
|
||||
cashappSubmit: any;
|
||||
loadingGooglePay = false;
|
||||
payments: any;
|
||||
cashAppPay: any;
|
||||
applePay: any;
|
||||
googlePay: any;
|
||||
conversionsSubscription: Subscription;
|
||||
conversions: any;
|
||||
|
||||
conversions: Record<string, number>;
|
||||
|
||||
// btcpay
|
||||
loadingBtcpayInvoice = false;
|
||||
invoice = undefined;
|
||||
@ -134,16 +136,16 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
private authService: AuthServiceMempool,
|
||||
private enterpriseService: EnterpriseService,
|
||||
) {
|
||||
this.accelerationUUID = insecureRandomUUID();
|
||||
this.isProdDomain = this.stateService.env.PROD_DOMAINS.indexOf(document.location.hostname) > -1;
|
||||
|
||||
// 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
|
||||
if (window.ApplePaySession) {
|
||||
// https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/checking_for_apple_pay_availability#overview
|
||||
if (window['ApplePaySession']) {
|
||||
this.applePayEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
ngOnInit(): void {
|
||||
this.authSubscription$ = this.authService.getAuth$().subscribe((auth) => {
|
||||
if (this.auth?.user?.userId !== auth?.user?.userId) {
|
||||
this.auth = auth;
|
||||
@ -168,13 +170,6 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.moveToStep('summary');
|
||||
}
|
||||
|
||||
this.servicesApiService.setupSquare$().subscribe(ids => {
|
||||
this.square = {
|
||||
appId: ids.squareAppId,
|
||||
locationId: ids.squareLocationId
|
||||
};
|
||||
});
|
||||
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
@ -182,7 +177,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
ngOnDestroy(): void {
|
||||
if (this.estimateSubscription) {
|
||||
this.estimateSubscription.unsubscribe();
|
||||
}
|
||||
@ -195,14 +190,17 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
if (changes.scrollEvent && this.scrollEvent) {
|
||||
this.scrollToElement('acceleratePreviewAnchor', 'start');
|
||||
}
|
||||
if (changes.accelerating) {
|
||||
if ((this.step === 'processing' || this.step === 'paid') && this.accelerating) {
|
||||
if (changes.accelerating && this.accelerating) {
|
||||
if (this.step === 'processing' || this.step === 'paid') {
|
||||
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;
|
||||
if (this.timeoutTimer) {
|
||||
clearTimeout(this.timeoutTimer);
|
||||
@ -211,6 +209,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.fetchEstimate();
|
||||
}
|
||||
if (this._step === 'checkout') {
|
||||
this.insertSquare();
|
||||
this.enterpriseService.goal(8);
|
||||
}
|
||||
if (this._step === 'checkout' && this.canPayWithBitcoin) {
|
||||
@ -220,12 +219,14 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.requestBTCPayInvoice();
|
||||
} else if (this._step === 'cashapp' && this.cashappEnabled) {
|
||||
this.loadingCashapp = true;
|
||||
this.insertSquare();
|
||||
this.setupSquare();
|
||||
this.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||
} else if (this._step === 'applepay' && this.applePayEnabled) {
|
||||
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.scrollToElementWithTimeout('confirm-title', 'center', 100);
|
||||
} else if (this._step === 'paid') {
|
||||
@ -234,7 +235,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
if (this.step === 'paid') {
|
||||
this.accelerateError = 'internal_server_error';
|
||||
}
|
||||
}, 120000)
|
||||
}, 120000);
|
||||
}
|
||||
this.hasDetails.emit(this._step === 'quote');
|
||||
}
|
||||
@ -252,7 +253,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.scrollToElement(id, position);
|
||||
}, timeout);
|
||||
}
|
||||
scrollToElement(id: string, position: ScrollLogicalPosition) {
|
||||
scrollToElement(id: string, position: ScrollLogicalPosition): void {
|
||||
const acceleratePreviewAnchor = document.getElementById(id);
|
||||
if (acceleratePreviewAnchor) {
|
||||
this.cd.markForCheck();
|
||||
@ -267,7 +268,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Accelerator
|
||||
*/
|
||||
fetchEstimate() {
|
||||
fetchEstimate(): void {
|
||||
if (this.estimateSubscription) {
|
||||
this.estimateSubscription.unsubscribe();
|
||||
}
|
||||
@ -331,7 +332,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
}),
|
||||
|
||||
catchError((response) => {
|
||||
catchError(() => {
|
||||
this.estimate = undefined;
|
||||
this.quoteError = `cannot_accelerate_tx`;
|
||||
this.estimateSubscription.unsubscribe();
|
||||
@ -367,6 +368,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.selectFeeRateIndex = index;
|
||||
this.userBid = Math.max(0, fee);
|
||||
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
|
||||
*/
|
||||
accelerateWithMempoolAccount(): void {
|
||||
if (!this.canPay || this.calculating) {
|
||||
if (!this.canPay || this.calculating || this.processing) {
|
||||
return;
|
||||
}
|
||||
this.processing = true;
|
||||
if (this.accelerationSubscription) {
|
||||
this.accelerationSubscription.unsubscribe();
|
||||
}
|
||||
this.accelerationSubscription = this.servicesApiService.accelerate$(
|
||||
this.tx.txid,
|
||||
this.userBid,
|
||||
this.accelerationUUID
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
this.showSuccess = true;
|
||||
@ -393,6 +396,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
this.moveToStep('paid');
|
||||
},
|
||||
error: (response) => {
|
||||
this.processing = false;
|
||||
this.accelerateError = response.error;
|
||||
}
|
||||
});
|
||||
@ -402,63 +406,74 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
* Square
|
||||
*/
|
||||
insertSquare(): void {
|
||||
//@ts-ignore
|
||||
if (window.Square) {
|
||||
if (!this.isProdDomain && !isDevMode()) {
|
||||
return;
|
||||
}
|
||||
if (window['Square']) {
|
||||
return;
|
||||
}
|
||||
let statsUrl = 'https://sandbox.web.squarecdn.com/v1/square.js';
|
||||
if (document.location.hostname === 'mempool-staging.fmt.mempool.space' ||
|
||||
document.location.hostname === 'mempool-staging.va1.mempool.space' ||
|
||||
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';
|
||||
if (this.isProdDomain) {
|
||||
statsUrl = '/square/v1/square.js';
|
||||
}
|
||||
|
||||
(function() {
|
||||
(function(): void {
|
||||
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);
|
||||
})();
|
||||
}
|
||||
setupSquare() {
|
||||
const init = () => {
|
||||
setupSquare(): void {
|
||||
if (!this.isProdDomain && !isDevMode()) {
|
||||
return;
|
||||
}
|
||||
const init = (): void => {
|
||||
this.initSquare();
|
||||
};
|
||||
|
||||
//@ts-ignore
|
||||
if (!window.Square) {
|
||||
console.debug('Square.js failed to load properly. Retrying in 1 second.');
|
||||
setTimeout(init, 1000);
|
||||
if (!window['Square']) {
|
||||
console.debug('Square.js failed to load properly. Retrying.');
|
||||
setTimeout(this.setupSquare.bind(this), 100);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
}
|
||||
async initSquare(): Promise<void> {
|
||||
try {
|
||||
//@ts-ignore
|
||||
this.payments = window.Square.payments(this.square.appId, this.square.locationId)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (this._step === 'cashapp' || urlParams.get('cash_request_id')) {
|
||||
await this.requestCashAppPayment();
|
||||
} else if (this._step === 'applepay') {
|
||||
await this.requestApplePayPayment();
|
||||
}
|
||||
this.servicesApiService.setupSquare$().subscribe({
|
||||
next: async (ids) => {
|
||||
this.payments = window['Square'].payments(ids.squareAppId, ids.squareLocationId);
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (this._step === 'cashapp' || urlParams.get('cash_request_id')) {
|
||||
await this.requestCashAppPayment();
|
||||
} 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) {
|
||||
console.debug('Error loading Square Payments', e);
|
||||
this.cashappError = true;
|
||||
return;
|
||||
this.accelerateError = 'cannot_setup_square';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* APPLE PAY
|
||||
*/
|
||||
async requestApplePayPayment() {
|
||||
async requestApplePayPayment(): Promise<void> {
|
||||
if (this.processing) {
|
||||
return;
|
||||
}
|
||||
if (this.conversionsSubscription) {
|
||||
this.conversionsSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
|
||||
this.processing = true;
|
||||
this.conversionsSubscription = this.stateService.conversions$.subscribe(
|
||||
async (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'`);
|
||||
// Try again
|
||||
setTimeout(this.requestApplePayPayment.bind(this), 500);
|
||||
this.processing = false;
|
||||
return;
|
||||
}
|
||||
this.loadingApplePay = false;
|
||||
@ -494,6 +510,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
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());
|
||||
@ -502,9 +519,11 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
tokenResult.token,
|
||||
cardTag,
|
||||
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
this.accelerationUUID
|
||||
costUSD
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.applePay) {
|
||||
this.applePay.destroy();
|
||||
@ -514,6 +533,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}, 1000);
|
||||
},
|
||||
error: (response) => {
|
||||
this.processing = false;
|
||||
this.accelerateError = response.error;
|
||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||
setTimeout(() => {
|
||||
@ -525,6 +545,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.processing = false;
|
||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||
if (tokenResult.errors) {
|
||||
errorMessage += ` and errors: ${JSON.stringify(
|
||||
@ -535,6 +556,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
this.processing = false;
|
||||
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) {
|
||||
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(
|
||||
async (conversions) => {
|
||||
this.conversions = conversions;
|
||||
@ -565,24 +686,21 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
amount: costUSD.toFixed(2),
|
||||
label: 'Total',
|
||||
pending: true,
|
||||
productUrl: `${redirectHostname}/tracker/${this.tx.txid}`,
|
||||
},
|
||||
button: { shape: 'semiround', size: 'small', theme: 'light'}
|
||||
productUrl: `${redirectHostname}/tx/${this.tx.txid}`,
|
||||
}
|
||||
});
|
||||
this.cashAppPay = await this.payments.cashAppPay(paymentRequest, {
|
||||
redirectURL: `${redirectHostname}/tracker/${this.tx.txid}`,
|
||||
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
|
||||
button: { shape: 'semiround', size: 'small', theme: 'light'}
|
||||
redirectURL: `${redirectHostname}/tx/${this.tx.txid}`,
|
||||
referenceId: `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`
|
||||
});
|
||||
|
||||
if (this.step === 'cashapp') {
|
||||
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'light', size: 'small', shape: 'semiround' })
|
||||
}
|
||||
await this.cashAppPay.attach(`#cash-app-pay`, { theme: 'dark' });
|
||||
this.loadingCashapp = false;
|
||||
|
||||
this.cashAppPay.addEventListener('ontokenization', event => {
|
||||
const { tokenResult, error } = event.detail;
|
||||
if (error) {
|
||||
this.processing = false;
|
||||
this.accelerateError = error;
|
||||
} else if (tokenResult.status === 'OK') {
|
||||
this.servicesApiService.accelerateWithCashApp$(
|
||||
@ -590,9 +708,10 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
tokenResult.token,
|
||||
tokenResult.details.cashAppPay.cashtag,
|
||||
tokenResult.details.cashAppPay.referenceId,
|
||||
this.accelerationUUID
|
||||
costUSD
|
||||
).subscribe({
|
||||
next: () => {
|
||||
this.processing = false;
|
||||
this.apiService.logAccelerationRequest$(this.tx.txid).subscribe();
|
||||
this.audioService.playSound('ascend-chime-cartoon');
|
||||
if (this.cashAppPay) {
|
||||
@ -607,6 +726,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
}, 1000);
|
||||
},
|
||||
error: (response) => {
|
||||
this.processing = false;
|
||||
this.accelerateError = response.error;
|
||||
if (!(response.status === 403 && response.error === 'not_available')) {
|
||||
setTimeout(() => {
|
||||
@ -626,7 +746,7 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
/**
|
||||
* BTCPay
|
||||
*/
|
||||
async requestBTCPayInvoice() {
|
||||
async requestBTCPayInvoice(): Promise<void> {
|
||||
this.servicesApiService.generateBTCPayAcceleratorInvoice$(this.tx.txid, this.userBid).pipe(
|
||||
switchMap(response => {
|
||||
return this.servicesApiService.retreiveInvoice$(response.btcpayInvoiceId);
|
||||
@ -656,54 +776,61 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
/**
|
||||
* UI events
|
||||
*/
|
||||
selectedOptionChanged(event) {
|
||||
selectedOptionChanged(event): void {
|
||||
this.selectedOption = event.target.id;
|
||||
}
|
||||
|
||||
get step() {
|
||||
get step(): CheckoutStep {
|
||||
return this._step;
|
||||
}
|
||||
|
||||
get paymentMethods() {
|
||||
return Object.keys(this.estimate?.availablePaymentMethods || {});
|
||||
get paymentMethods(): PaymentMethod[] {
|
||||
return Object.keys(this.estimate?.availablePaymentMethods || {}) as PaymentMethod[];
|
||||
}
|
||||
|
||||
get couldPayWithBitcoin() {
|
||||
get couldPayWithBitcoin(): boolean {
|
||||
return !!this.estimate?.availablePaymentMethods?.bitcoin;
|
||||
}
|
||||
|
||||
get couldPayWithCashapp() {
|
||||
get couldPayWithCashapp(): boolean {
|
||||
if (!this.cashappEnabled) {
|
||||
return false;
|
||||
}
|
||||
return !!this.estimate?.availablePaymentMethods?.cashapp;
|
||||
}
|
||||
|
||||
get couldPayWithApplePay() {
|
||||
get couldPayWithApplePay(): boolean {
|
||||
if (!this.applePayEnabled) {
|
||||
return false;
|
||||
}
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
return !!this.estimate?.availablePaymentMethods?.balance;
|
||||
}
|
||||
|
||||
get couldPay() {
|
||||
return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp || this.couldPayWithApplePay;
|
||||
get couldPay(): boolean {
|
||||
return this.couldPayWithBalance || this.couldPayWithBitcoin || this.couldPayWithCashapp || this.couldPayWithApplePay || this.couldPayWithGooglePay;
|
||||
}
|
||||
|
||||
get canPayWithBitcoin() {
|
||||
get canPayWithBitcoin(): boolean {
|
||||
const paymentMethod = this.estimate?.availablePaymentMethods?.bitcoin;
|
||||
return paymentMethod && this.cost >= paymentMethod.min && this.cost <= paymentMethod.max;
|
||||
}
|
||||
|
||||
get canPayWithCashapp() {
|
||||
if (!this.cashappEnabled || !this.conversions) {
|
||||
get canPayWithCashapp(): boolean {
|
||||
if (!this.cashappEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -718,8 +845,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
|
||||
get canPayWithApplePay() {
|
||||
if (!this.applePayEnabled || !this.conversions) {
|
||||
get canPayWithApplePay(): boolean {
|
||||
if (!this.applePayEnabled || !this.conversions || (!this.isProdDomain && !isDevMode())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -734,7 +861,23 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
get canPay() {
|
||||
return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp || this.canPayWithApplePay;
|
||||
get canPay(): boolean {
|
||||
return this.canPayWithBalance || this.canPayWithBitcoin || this.canPayWithCashapp || this.canPayWithApplePay || this.canPayWithGooglePay;
|
||||
}
|
||||
|
||||
get hasAccessToBalanceMode() {
|
||||
get hasAccessToBalanceMode(): boolean {
|
||||
return this.isLoggedIn() && this.estimate?.hasAccess;
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
</p>
|
||||
</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>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { AccelerationEstimate, RateOption } from './accelerate-checkout.component';
|
||||
import { Transaction } from '@interfaces/electrs.interface';
|
||||
import { AccelerationEstimate, RateOption } from '@components/accelerate-checkout/accelerate-checkout.component';
|
||||
|
||||
interface GraphBar {
|
||||
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="timeline-wrapper">
|
||||
@if (!tx.status.confirmed) {
|
||||
@if (!tx.status.confirmed || canceled) {
|
||||
<div class="timeline">
|
||||
<div class="intervals">
|
||||
<div class="node-spacer"></div>
|
||||
@ -8,8 +8,8 @@
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
@if (eta) {
|
||||
~<app-time [time]="eta?.wait / 1000"></app-time> <!-- <span *ngIf="accelerateRatio > 1" class="compare"> ({{ accelerateRatio }}x faster)</span> -->
|
||||
@if (eta && !canceled) {
|
||||
~<app-time [time]="eta?.wait / 1000"></app-time>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -19,16 +19,20 @@
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval-spacer"></div>
|
||||
<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 class="interval-spacer">
|
||||
</div>
|
||||
<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 animate"></div>
|
||||
<div class="shape"></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>
|
||||
@ -38,18 +42,16 @@
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
<app-time [time]="acceleratedAt - transactionTime"></app-time>
|
||||
<app-time [time]="firstSeenToAccelerated"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-spacer"></div>
|
||||
<div class="interval">
|
||||
<div class="interval-time">
|
||||
@if (tx.status.confirmed) {
|
||||
<div class="interval-time">
|
||||
<app-time [time]="tx.status.block_time - acceleratedAt"></app-time>
|
||||
</div>
|
||||
} @else if (standardETA && !tx.status.confirmed) {
|
||||
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
|
||||
<app-time [time]="acceleratedToMined"></app-time>
|
||||
} @else if (eta && canceled) {
|
||||
~<app-time [time]="eta?.wait / 1000"></app-time>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -58,7 +60,7 @@
|
||||
<div class="nodes">
|
||||
<div class="node" [id]="'first-seen'">
|
||||
<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>
|
||||
<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="seen-to-acc"></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>
|
||||
@if (tx.status.confirmed) {
|
||||
@if (tx.status.confirmed && !canceled) {
|
||||
<div class="acc-to-confirmed right"></div>
|
||||
} @else {
|
||||
<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>
|
||||
@if (!tx.status.confirmed) {
|
||||
<div class="connector down loading"></div>
|
||||
@if (!tx.status.confirmed || canceled) {
|
||||
<div class="connector down" [class.loading]="!canceled"></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="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) {
|
||||
<span i18n="transaction.audit.accelerated">Accelerated</span>{{ "" }}
|
||||
}
|
||||
@if (useAbsoluteTime) {
|
||||
<span>{{ acceleratedAt * 1000 | date }}</span>
|
||||
} @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 class="interval-spacer">
|
||||
@if (tx.status.confirmed) {
|
||||
@if (tx.status.confirmed && !canceled) {
|
||||
<div class="acc-to-confirmed"></div>
|
||||
} @else {
|
||||
<div class="seen-to-acc"></div>
|
||||
}
|
||||
</div>
|
||||
<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>
|
||||
} @else {
|
||||
<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>
|
||||
@if (tx.status.confirmed) {
|
||||
@ -130,4 +135,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-acceleration-timeline-tooltip
|
||||
[accelerationInfo]="hoverInfo"
|
||||
[cursorPosition]="tooltipPosition"
|
||||
></app-acceleration-timeline-tooltip>
|
||||
|
||||
</div>
|
||||
|
@ -129,6 +129,9 @@
|
||||
margin-left: calc(-4em + 5px);
|
||||
animation: goFasterLeft 0.8s infinite linear;
|
||||
}
|
||||
&.no-animation {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.left {
|
||||
@ -152,9 +155,16 @@
|
||||
margin-bottom: -8px;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
background: transparent;
|
||||
transition: background-color 300ms, padding 300ms;
|
||||
|
||||
&.hovering {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: relative;
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Component, Input, OnInit, OnChanges } from '@angular/core';
|
||||
import { ETA } from '../../services/eta.service';
|
||||
import { Transaction } from '../../interfaces/electrs.interface';
|
||||
import { Component, Input, OnInit, OnChanges, HostListener } from '@angular/core';
|
||||
import { ETA } from '@app/services/eta.service';
|
||||
import { Transaction } from '@interfaces/electrs.interface';
|
||||
import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
|
||||
import { MiningService } from '@app/services/mining.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-timeline',
|
||||
@ -9,47 +11,82 @@ import { Transaction } from '../../interfaces/electrs.interface';
|
||||
})
|
||||
export class AccelerationTimelineComponent implements OnInit, OnChanges {
|
||||
@Input() transactionTime: number;
|
||||
@Input() acceleratedAt: number;
|
||||
@Input() tx: Transaction;
|
||||
@Input() accelerationInfo: Acceleration;
|
||||
@Input() eta: ETA;
|
||||
// A mined transaction has standard ETA and accelerated ETA undefined
|
||||
// A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet)
|
||||
@Input() standardETA: number;
|
||||
@Input() acceleratedETA: number;
|
||||
@Input() canceled: boolean;
|
||||
|
||||
acceleratedAt: number;
|
||||
now: number;
|
||||
accelerateRatio: number;
|
||||
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 {
|
||||
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
|
||||
this.now = Math.floor(new Date().getTime() / 1000);
|
||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
||||
this.updateTimes();
|
||||
|
||||
this.interval = window.setInterval(() => {
|
||||
this.now = Math.floor(new Date().getTime() / 1000);
|
||||
this.useAbsoluteTime = this.tx.status.block_time < this.now - 7 * 24 * 3600;
|
||||
}, 60000);
|
||||
this.miningService.getPools().subscribe(pools => {
|
||||
for (const pool of pools) {
|
||||
this.poolsData[pool.unique_id] = pool;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes): void {
|
||||
// Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65
|
||||
|
||||
// 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));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
this.updateTimes();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
clearInterval(this.interval);
|
||||
updateTimes(): void {
|
||||
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 { EChartsOption } from '../../../graphs/echarts';
|
||||
import { EChartsOption } from '@app/graphs/echarts';
|
||||
import { Observable, Subject, Subscription, combineLatest, fromEvent, merge, share } from 'rxjs';
|
||||
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 { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '../../../shared/graphs.utils';
|
||||
import { StorageService } from '../../../services/storage.service';
|
||||
import { MiningService } from '../../../services/mining.service';
|
||||
import { download, formatterXAxis, formatterXAxisLabel, formatterXAxisTimeCategory } from '@app/shared/graphs.utils';
|
||||
import { StorageService } from '@app/services/storage.service';
|
||||
import { MiningService } from '@app/services/mining.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Acceleration } from '../../../interfaces/node-api.interface';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { Acceleration } from '@interfaces/node-api.interface';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-fees-graph',
|
||||
@ -23,7 +23,7 @@ import { RelativeUrlPipe } from '../../../shared/pipes/relative-url/relative-url
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@ -264,7 +264,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnChanges, OnDest
|
||||
type: 'bar',
|
||||
barWidth: '90%',
|
||||
large: true,
|
||||
barMinHeight: 1,
|
||||
barMinHeight: 3,
|
||||
},
|
||||
],
|
||||
dataZoom: (this.widget || data.length === 0 )? undefined : [{
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
|
||||
export type AccelerationStats = {
|
||||
totalRequested: number;
|
||||
|
@ -4,8 +4,8 @@
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="acceleration-list" *ngIf="accelerationList$ | async as accelerations">
|
||||
<table *ngIf="!accelerations || accelerations.length; else noData" class="table table-borderless table-fixed">
|
||||
<div class="acceleration-list" *ngIf="{ accelerations: accelerationList$ | async } as state">
|
||||
<table *ngIf="nonEmptyAccelerations; else noData" class="table table-borderless table-fixed">
|
||||
<thead>
|
||||
<th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th>
|
||||
<ng-container *ngIf="pending">
|
||||
@ -21,8 +21,8 @@
|
||||
<th class="date text-right" i18n="accelerator.requested" *ngIf="!this.widget">Requested</th>
|
||||
</ng-container>
|
||||
</thead>
|
||||
<tbody *ngIf="accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tr *ngFor="let acceleration of accelerations; let i= index;">
|
||||
<tbody *ngIf="state.accelerations && nonEmptyAccelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
<tr *ngFor="let acceleration of state.accelerations; let i= index;">
|
||||
<td class="txid text-left">
|
||||
<a [routerLink]="['/tx' | relativeUrl, acceleration.txid]">
|
||||
<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>
|
||||
</td>
|
||||
<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 class="time text-right">
|
||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
||||
@ -41,7 +41,7 @@
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!pending">
|
||||
<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 *ngIf="acceleration.boost == null" class="fee text-right">
|
||||
~
|
||||
@ -62,8 +62,9 @@
|
||||
</td>
|
||||
<td class="status text-right">
|
||||
<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('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="accelerator.completed">Completed</ng-container><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="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 class="date text-right" *ngIf="!this.widget">
|
||||
<app-time kind="since" [time]="acceleration.added" [fastRender]="true" [showTooltip]="true"></app-time>
|
||||
@ -72,22 +73,47 @@
|
||||
</tr>
|
||||
</tbody>
|
||||
<ng-template #skeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="txid text-left">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td class="fee text-right">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td class="fee-delta text-right">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
<td class="status text-right">
|
||||
<span class="skeleton-loader" style="max-width: 75px"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@if (!pending) {
|
||||
<tbody>
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="txid text-left">
|
||||
<span class="skeleton-loader" style="max-width: 200px"></span>
|
||||
</td>
|
||||
<td class="fee text-right">
|
||||
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||
</td>
|
||||
<td class="block text-right">
|
||||
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||
</td>
|
||||
<td class="pool text-right" *ngIf="!this.widget">
|
||||
<span class="skeleton-loader" style="max-width: 100px"></span>
|
||||
</td>
|
||||
<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>
|
||||
</table>
|
||||
|
||||
|
@ -85,8 +85,8 @@ tr, td, th {
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
|
@ -1,12 +1,12 @@
|
||||
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 { Acceleration, BlockExtended, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { BehaviorSubject, Observable, Subscription, catchError, combineLatest, filter, of, switchMap, tap, throttleTime, timer } from 'rxjs';
|
||||
import { Acceleration, BlockExtended, SinglePoolStats } from '@interfaces/node-api.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MiningService } from '../../../services/mining.service';
|
||||
import { MiningService } from '@app/services/mining.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-accelerations-list',
|
||||
@ -32,6 +32,7 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
||||
dir: 'rtl' | 'ltr' = 'ltr';
|
||||
paramSubscription: Subscription;
|
||||
pools: { [id: number]: SinglePoolStats } = {};
|
||||
nonEmptyAccelerations: boolean = true;
|
||||
|
||||
constructor(
|
||||
private servicesApiService: ServicesApiServices,
|
||||
@ -50,12 +51,21 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.miningService.getPools().subscribe(pools => {
|
||||
for (const pool of pools) {
|
||||
this.pools[pool.unique_id] = pool;
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.widget) {
|
||||
this.websocketService.want(['blocks']);
|
||||
this.seoService.setTitle($localize`:@@02573b6980a2d611b4361a2595a4447e390058cd:Accelerations`);
|
||||
|
||||
this.paramSubscription = this.route.params.pipe(
|
||||
tap(params => {
|
||||
this.paramSubscription = combineLatest([
|
||||
this.route.params,
|
||||
timer(0),
|
||||
]).pipe(
|
||||
tap(([params]) => {
|
||||
this.page = +params['page'] || 1;
|
||||
this.pageSubject.next(this.page);
|
||||
})
|
||||
@ -82,12 +92,6 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
||||
).subscribe(() => {
|
||||
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()];
|
||||
@ -115,6 +119,7 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
||||
for (const acc of accelerations) {
|
||||
acc.boost = acc.boostCost != null ? acc.boostCost : acc.bidBoost;
|
||||
}
|
||||
this.nonEmptyAccelerations = accelerations.length > 0;
|
||||
if (this.widget) {
|
||||
return of(accelerations.slice(0, 6));
|
||||
} else {
|
||||
@ -146,4 +151,4 @@ export class AccelerationsListComponent implements OnInit, OnDestroy {
|
||||
this.paramSubscription?.unsubscribe();
|
||||
this.keyNavigationSubscription?.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { OpenGraphService } from '../../../services/opengraph.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { OpenGraphService } from '@app/services/opengraph.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { Acceleration, BlockExtended } from '@interfaces/node-api.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs';
|
||||
import { Color } from '../../block-overview-graph/sprite-types';
|
||||
import { hexToColor } from '../../block-overview-graph/utils';
|
||||
import TxView from '../../block-overview-graph/tx-view';
|
||||
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '../../../app.constants';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
import { detectWebGL } from '../../../shared/graphs.utils';
|
||||
import { AudioService } from '../../../services/audio.service';
|
||||
import { ThemeService } from '../../../services/theme.service';
|
||||
import { Color } from '@components/block-overview-graph/sprite-types';
|
||||
import { hexToColor } from '@components/block-overview-graph/utils';
|
||||
import TxView from '@components/block-overview-graph/tx-view';
|
||||
import { feeLevels, defaultMempoolFeeColors, contrastMempoolFeeColors } from '@app/app.constants';
|
||||
import { ServicesApiServices } from '@app/services/services-api.service';
|
||||
import { detectWebGL } from '@app/shared/graphs.utils';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { ThemeService } from '@app/services/theme.service';
|
||||
|
||||
const acceleratedColor: Color = hexToColor('8F5FF6');
|
||||
const normalColors = defaultMempoolFeeColors.map(hex => hexToColor(hex + '5F'));
|
||||
|
@ -1,7 +1,7 @@
|
||||
@if (chartOnly) {
|
||||
<ng-container *ngTemplateOutlet="pieChart"></ng-container>
|
||||
} @else {
|
||||
<table>
|
||||
<table style="width: 100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<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 class="field-value" [class]="chartPositionLeft ? 'chart-left' : ''">
|
||||
<div class="effective-fee-container">
|
||||
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
|
||||
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||
@if (accelerationInfo?.acceleratedFeeRate && (!effectiveFeeRate || accelerationInfo.acceleratedFeeRate >= effectiveFeeRate)) {
|
||||
<app-fee-rate class="oobFees" [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||
} @else {
|
||||
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||
<app-fee-rate class="oobFees" [fee]="effectiveFeeRate"></app-fee-rate>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td class="pie-chart" rowspan="2" *ngIf="!chartPositionLeft">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
@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>
|
||||
</div>
|
||||
@ -36,7 +36,7 @@
|
||||
<tr>
|
||||
<td colspan="3" class="pt-0">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -61,4 +61,8 @@
|
||||
& > div, & > div > svg {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
.oobFees {
|
||||
color: #905cf4;
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter } from '@angular/core';
|
||||
import { Transaction } from '../../../interfaces/electrs.interface';
|
||||
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
||||
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
|
||||
import { MiningStats } from '../../../services/mining.service';
|
||||
import { Component, ChangeDetectionStrategy, Input, Output, OnChanges, SimpleChanges, EventEmitter, ChangeDetectorRef } from '@angular/core';
|
||||
import { Transaction } from '@interfaces/electrs.interface';
|
||||
import { Acceleration, SinglePoolStats } from '@interfaces/node-api.interface';
|
||||
import { EChartsOption, PieSeriesOption } from '@app/graphs/echarts';
|
||||
import { MiningStats } from '@app/services/mining.service';
|
||||
|
||||
function lighten(color, p): { r, g, b } {
|
||||
return {
|
||||
@ -23,7 +23,8 @@ function toRGB({r,g,b}): string {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ActiveAccelerationBox implements OnChanges {
|
||||
@Input() tx: Transaction;
|
||||
@Input() acceleratedBy?: number[];
|
||||
@Input() effectiveFeeRate?: number;
|
||||
@Input() accelerationInfo: Acceleration;
|
||||
@Input() miningStats: MiningStats;
|
||||
@Input() pools: number[];
|
||||
@ -41,10 +42,12 @@ export class ActiveAccelerationBox implements OnChanges {
|
||||
timespan = '';
|
||||
chartInstance: any = undefined;
|
||||
|
||||
constructor() {}
|
||||
constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
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) {
|
||||
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 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) => {
|
||||
const pool = pools[poolId];
|
||||
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(
|
||||
pool.lastEstimatedHashrate,
|
||||
toRGB(lighten({ r: 147, g: 57, b: 244 }, index * .08)),
|
||||
color,
|
||||
`<b style="color: white">${pool.name} (${poolShare}%)</b>`,
|
||||
true,
|
||||
) as PieSeriesOption);
|
||||
})
|
||||
});
|
||||
this.acceleratedByPercentage = ((totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate) * 100).toFixed(1) + '%';
|
||||
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
|
||||
data.push(getDataItem(
|
||||
@ -127,6 +141,7 @@ export class ActiveAccelerationBox implements OnChanges {
|
||||
}
|
||||
]
|
||||
};
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
@ -139,4 +154,4 @@ export class ActiveAccelerationBox implements OnChanges {
|
||||
onToggleCpfp(): void {
|
||||
this.toggleCpfp.emit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { Acceleration } from '../../../interfaces/node-api.interface';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { Acceleration } from '@interfaces/node-api.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
|
||||
@Component({
|
||||
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 { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { echarts, EChartsOption } from '@app/graphs/echarts';
|
||||
import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
|
||||
import { catchError, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { AddressTxSummary, ChainStats } from '@interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
import { AmountShortenerPipe } from '@app/shared/pipes/amount-shortener.pipe';
|
||||
import { Router } from '@angular/router';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { PriceService } from '../../services/price.service';
|
||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
||||
import { RelativeUrlPipe } from '@app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { PriceService } from '@app/services/price.service';
|
||||
import { FiatCurrencyPipe } from '@app/shared/pipes/fiat-currency.pipe';
|
||||
|
||||
const periodSeconds = {
|
||||
'1d': (60 * 60 * 24),
|
||||
@ -30,7 +29,7 @@ const periodSeconds = {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
z-index: 99;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@ -45,6 +44,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
@Input() right: number | string = 10;
|
||||
@Input() left: number | string = 70;
|
||||
@Input() widget: boolean = false;
|
||||
@Input() defaultFiat: boolean = false;
|
||||
|
||||
data: any[] = [];
|
||||
fiatData: any[] = [];
|
||||
@ -77,7 +77,6 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private priceService: PriceService,
|
||||
private fiatCurrencyPipe: FiatCurrencyPipe,
|
||||
private fiatShortenerPipe: FiatShortenerPipe,
|
||||
private zone: NgZone,
|
||||
) {}
|
||||
|
||||
@ -86,6 +85,9 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
if (!this.addressSummary$ && (!this.address || !this.stats)) {
|
||||
return;
|
||||
}
|
||||
if (changes.defaultFiat) {
|
||||
this.selected['Fiat'] = !!this.defaultFiat;
|
||||
}
|
||||
if (changes.address || changes.isPubkey || changes.addressSummary$ || changes.stats) {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
@ -147,7 +149,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
if (!summary) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const total = this.stats ? (this.stats.funded_txo_sum - this.stats.spent_txo_sum) : summary.reduce((acc, tx) => acc + tx.value, 0);
|
||||
let runningTotal = total;
|
||||
const processData = summary.map(d => {
|
||||
@ -161,7 +163,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
d
|
||||
};
|
||||
}).reverse();
|
||||
|
||||
|
||||
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]);
|
||||
|
||||
@ -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 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 = {
|
||||
color: [
|
||||
new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
@ -245,21 +250,22 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
let tooltip = '<div>';
|
||||
|
||||
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) {
|
||||
const header = data.length === 1
|
||||
? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}`
|
||||
: `${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 formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD');
|
||||
|
||||
|
||||
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 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;
|
||||
}.bind(this)
|
||||
},
|
||||
@ -311,18 +317,21 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
formatter: (val): string => {
|
||||
let valSpan = maxValue - (this.period === 'all' ? 0 : minValue);
|
||||
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) {
|
||||
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) {
|
||||
return `${(val / 100_000_000).toFixed(1)} BTC`;
|
||||
} else if (valSpan > 10_000_000) {
|
||||
return `${(val / 100_000_000).toFixed(2)} BTC`;
|
||||
} 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`;
|
||||
} 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: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: function(val) {
|
||||
return this.fiatShortenerPipe.transform(val, null, 'USD');
|
||||
return `$${this.amountShortenerPipe.transform(val, 0, undefined, true)}`;
|
||||
}.bind(this)
|
||||
},
|
||||
splitLine: {
|
||||
@ -440,7 +449,7 @@ export class AddressGraphComponent implements OnChanges, OnDestroy {
|
||||
right: this.right,
|
||||
}] : undefined
|
||||
};
|
||||
|
||||
|
||||
if (this.chartInstance) {
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectorRef, HostListener } from '@angular/core';
|
||||
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 { Address, Transaction } from '../../interfaces/electrs.interface';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AudioService } from '../../services/audio.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Address, Transaction } from '@interfaces/electrs.interface';
|
||||
import { WebsocketService } from '@app/services/websocket.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { AudioService } from '@app/services/audio.service';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { of, Subscription, forkJoin } from 'rxjs';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { AddressInformation } from '../../interfaces/node-api.interface';
|
||||
import { SeoService } from '@app/services/seo.service';
|
||||
import { AddressInformation } from '@interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-group',
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
|
||||
import { Vin, Vout } from '../../interfaces/electrs.interface';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AddressType, AddressTypeInfo } from '../../shared/address-utils';
|
||||
import { Vin, Vout } from '@interfaces/electrs.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { AddressType, AddressTypeInfo } from '@app/shared/address-utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-address-labels',
|
||||
@ -55,7 +55,7 @@ export class AddressLabelsComponent implements OnChanges {
|
||||
}
|
||||
|
||||
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) {
|
||||
const script = address?.scripts.values().next().value;
|
||||
if (script.template?.label) {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { Address, AddressTxSummary } from '@interfaces/electrs.interface';
|
||||
import { ElectrsApiService } from '@app/services/electrs-api.service';
|
||||
import { Observable, Subscription, catchError, map, of, switchMap, zip } from 'rxjs';
|
||||
import { PriceService } from '../../services/price.service';
|
||||
import { PriceService } from '@app/services/price.service';
|
||||
|
||||
@Component({
|
||||
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