Merge pull request #3995 from mempool/mononaut/acceleration-viz
Acceleration visualization
This commit is contained in:
commit
989e4832cc
@ -137,5 +137,9 @@
|
|||||||
"trusted",
|
"trusted",
|
||||||
"servers"
|
"servers"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"MEMPOOL_SERVICES": {
|
||||||
|
"API": "https://mempool.space/api",
|
||||||
|
"ACCELERATIONS": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8
backend/rust-gbt/index.d.ts
vendored
8
backend/rust-gbt/index.d.ts
vendored
@ -12,6 +12,10 @@ export interface ThreadTransaction {
|
|||||||
effectiveFeePerVsize: number
|
effectiveFeePerVsize: number
|
||||||
inputs: Array<number>
|
inputs: Array<number>
|
||||||
}
|
}
|
||||||
|
export interface ThreadAcceleration {
|
||||||
|
uid: number
|
||||||
|
delta: number
|
||||||
|
}
|
||||||
export class GbtGenerator {
|
export class GbtGenerator {
|
||||||
constructor()
|
constructor()
|
||||||
/**
|
/**
|
||||||
@ -19,13 +23,13 @@ export class GbtGenerator {
|
|||||||
*
|
*
|
||||||
* Rejects if the thread panics or if the Mutex is poisoned.
|
* Rejects if the thread panics or if the Mutex is poisoned.
|
||||||
*/
|
*/
|
||||||
make(mempool: Array<ThreadTransaction>, maxUid: number): Promise<GbtResult>
|
make(mempool: Array<ThreadTransaction>, accelerations: Array<ThreadAcceleration>, maxUid: number): Promise<GbtResult>
|
||||||
/**
|
/**
|
||||||
* # Errors
|
* # Errors
|
||||||
*
|
*
|
||||||
* Rejects if the thread panics or if the Mutex is poisoned.
|
* Rejects if the thread panics or if the Mutex is poisoned.
|
||||||
*/
|
*/
|
||||||
update(newTxs: Array<ThreadTransaction>, removeTxs: Array<number>, maxUid: number): Promise<GbtResult>
|
update(newTxs: Array<ThreadTransaction>, removeTxs: Array<number>, accelerations: Array<ThreadAcceleration>, maxUid: number): Promise<GbtResult>
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* The result from calling the gbt function.
|
* The result from calling the gbt function.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
u32_hasher_types::{u32hashset_new, U32HasherState},
|
u32_hasher_types::{u32hashset_new, U32HasherState},
|
||||||
ThreadTransaction,
|
ThreadTransaction, thread_acceleration::ThreadAcceleration,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
@ -88,44 +88,49 @@ impl Ord for AuditTransaction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn calc_fee_rate(fee: f64, vsize: f64) -> f64 {
|
fn calc_fee_rate(fee: u64, vsize: f64) -> f64 {
|
||||||
fee / (if vsize == 0.0 { 1.0 } else { vsize })
|
(fee as f64) / (if vsize == 0.0 { 1.0 } else { vsize })
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuditTransaction {
|
impl AuditTransaction {
|
||||||
pub fn from_thread_transaction(tx: &ThreadTransaction) -> Self {
|
pub fn from_thread_transaction(tx: &ThreadTransaction, maybe_acceleration: Option<Option<&ThreadAcceleration>>) -> Self {
|
||||||
|
let fee_delta = match maybe_acceleration {
|
||||||
|
Some(Some(acceleration)) => acceleration.delta,
|
||||||
|
_ => 0.0
|
||||||
|
};
|
||||||
|
let fee = (tx.fee as u64) + (fee_delta as u64);
|
||||||
// rounded up to the nearest integer
|
// rounded up to the nearest integer
|
||||||
let is_adjusted = tx.weight < (tx.sigops * 20);
|
let is_adjusted = tx.weight < (tx.sigops * 20);
|
||||||
let sigop_adjusted_vsize = ((tx.weight + 3) / 4).max(tx.sigops * 5);
|
let sigop_adjusted_vsize = ((tx.weight + 3) / 4).max(tx.sigops * 5);
|
||||||
let sigop_adjusted_weight = tx.weight.max(tx.sigops * 20);
|
let sigop_adjusted_weight = tx.weight.max(tx.sigops * 20);
|
||||||
let effective_fee_per_vsize = if is_adjusted {
|
let effective_fee_per_vsize = if is_adjusted || fee_delta > 0.0 {
|
||||||
calc_fee_rate(tx.fee, f64::from(sigop_adjusted_weight) / 4.0)
|
calc_fee_rate(fee, f64::from(sigop_adjusted_weight) / 4.0)
|
||||||
} else {
|
} else {
|
||||||
tx.effective_fee_per_vsize
|
tx.effective_fee_per_vsize
|
||||||
};
|
};
|
||||||
Self {
|
Self {
|
||||||
uid: tx.uid,
|
uid: tx.uid,
|
||||||
order: tx.order,
|
order: tx.order,
|
||||||
fee: tx.fee as u64,
|
fee,
|
||||||
weight: tx.weight,
|
weight: tx.weight,
|
||||||
sigop_adjusted_weight,
|
sigop_adjusted_weight,
|
||||||
sigop_adjusted_vsize,
|
sigop_adjusted_vsize,
|
||||||
sigops: tx.sigops,
|
sigops: tx.sigops,
|
||||||
adjusted_fee_per_vsize: calc_fee_rate(tx.fee, f64::from(sigop_adjusted_vsize)),
|
adjusted_fee_per_vsize: calc_fee_rate(fee, f64::from(sigop_adjusted_vsize)),
|
||||||
effective_fee_per_vsize,
|
effective_fee_per_vsize,
|
||||||
dependency_rate: f64::INFINITY,
|
dependency_rate: f64::INFINITY,
|
||||||
inputs: tx.inputs.clone(),
|
inputs: tx.inputs.clone(),
|
||||||
relatives_set_flag: false,
|
relatives_set_flag: false,
|
||||||
ancestors: u32hashset_new(),
|
ancestors: u32hashset_new(),
|
||||||
children: u32hashset_new(),
|
children: u32hashset_new(),
|
||||||
ancestor_fee: tx.fee as u64,
|
ancestor_fee: fee,
|
||||||
ancestor_sigop_adjusted_weight: sigop_adjusted_weight,
|
ancestor_sigop_adjusted_weight: sigop_adjusted_weight,
|
||||||
ancestor_sigop_adjusted_vsize: sigop_adjusted_vsize,
|
ancestor_sigop_adjusted_vsize: sigop_adjusted_vsize,
|
||||||
ancestor_sigops: tx.sigops,
|
ancestor_sigops: tx.sigops,
|
||||||
score: 0.0,
|
score: 0.0,
|
||||||
used: false,
|
used: false,
|
||||||
modified: false,
|
modified: false,
|
||||||
dirty: effective_fee_per_vsize != tx.effective_fee_per_vsize,
|
dirty: effective_fee_per_vsize != tx.effective_fee_per_vsize || fee_delta > 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +161,7 @@ impl AuditTransaction {
|
|||||||
// grows, so if we think of 0 as "grew infinitely" then dependency_rate would be
|
// grows, so if we think of 0 as "grew infinitely" then dependency_rate would be
|
||||||
// the smaller of the two. If either side is NaN, the other side is returned.
|
// the smaller of the two. If either side is NaN, the other side is returned.
|
||||||
self.dependency_rate.min(calc_fee_rate(
|
self.dependency_rate.min(calc_fee_rate(
|
||||||
self.ancestor_fee as f64,
|
self.ancestor_fee,
|
||||||
f64::from(self.ancestor_sigop_adjusted_weight) / 4.0,
|
f64::from(self.ancestor_sigop_adjusted_weight) / 4.0,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -172,7 +177,7 @@ impl AuditTransaction {
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn calc_new_score(&mut self) {
|
fn calc_new_score(&mut self) {
|
||||||
self.score = self.adjusted_fee_per_vsize.min(calc_fee_rate(
|
self.score = self.adjusted_fee_per_vsize.min(calc_fee_rate(
|
||||||
self.ancestor_fee as f64,
|
self.ancestor_fee,
|
||||||
f64::from(self.ancestor_sigop_adjusted_vsize),
|
f64::from(self.ancestor_sigop_adjusted_vsize),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ use tracing::{info, trace};
|
|||||||
use crate::{
|
use crate::{
|
||||||
audit_transaction::{partial_cmp_uid_score, AuditTransaction},
|
audit_transaction::{partial_cmp_uid_score, AuditTransaction},
|
||||||
u32_hasher_types::{u32hashset_new, u32priority_queue_with_capacity, U32HasherState},
|
u32_hasher_types::{u32hashset_new, u32priority_queue_with_capacity, U32HasherState},
|
||||||
GbtResult, ThreadTransactionsMap,
|
GbtResult, ThreadTransactionsMap, thread_acceleration::ThreadAcceleration,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_BLOCK_WEIGHT_UNITS: u32 = 4_000_000 - 4_000;
|
const MAX_BLOCK_WEIGHT_UNITS: u32 = 4_000_000 - 4_000;
|
||||||
@ -53,7 +53,13 @@ impl Ord for TxPriority {
|
|||||||
// TODO: Make gbt smaller to fix these lints.
|
// TODO: Make gbt smaller to fix these lints.
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
#[allow(clippy::cognitive_complexity)]
|
#[allow(clippy::cognitive_complexity)]
|
||||||
pub fn gbt(mempool: &mut ThreadTransactionsMap, max_uid: usize) -> GbtResult {
|
pub fn gbt(mempool: &mut ThreadTransactionsMap, accelerations: &[ThreadAcceleration], max_uid: usize) -> GbtResult {
|
||||||
|
let mut indexed_accelerations = Vec::with_capacity(max_uid + 1);
|
||||||
|
indexed_accelerations.resize(max_uid + 1, None);
|
||||||
|
for acceleration in accelerations {
|
||||||
|
indexed_accelerations[acceleration.uid as usize] = Some(acceleration);
|
||||||
|
}
|
||||||
|
|
||||||
let mempool_len = mempool.len();
|
let mempool_len = mempool.len();
|
||||||
let mut audit_pool: AuditPool = Vec::with_capacity(max_uid + 1);
|
let mut audit_pool: AuditPool = Vec::with_capacity(max_uid + 1);
|
||||||
audit_pool.resize(max_uid + 1, None);
|
audit_pool.resize(max_uid + 1, None);
|
||||||
@ -63,7 +69,8 @@ pub fn gbt(mempool: &mut ThreadTransactionsMap, max_uid: usize) -> GbtResult {
|
|||||||
|
|
||||||
info!("Initializing working structs");
|
info!("Initializing working structs");
|
||||||
for (uid, tx) in &mut *mempool {
|
for (uid, tx) in &mut *mempool {
|
||||||
let audit_tx = AuditTransaction::from_thread_transaction(tx);
|
let acceleration = indexed_accelerations.get(*uid as usize);
|
||||||
|
let audit_tx = AuditTransaction::from_thread_transaction(tx, acceleration.copied());
|
||||||
// Safety: audit_pool and mempool_stack must always contain the same transactions
|
// Safety: audit_pool and mempool_stack must always contain the same transactions
|
||||||
audit_pool[*uid as usize] = Some(ManuallyDrop::new(audit_tx));
|
audit_pool[*uid as usize] = Some(ManuallyDrop::new(audit_tx));
|
||||||
mempool_stack.push(*uid);
|
mempool_stack.push(*uid);
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
use napi::bindgen_prelude::Result;
|
use napi::bindgen_prelude::Result;
|
||||||
use napi_derive::napi;
|
use napi_derive::napi;
|
||||||
use thread_transaction::ThreadTransaction;
|
use thread_transaction::ThreadTransaction;
|
||||||
|
use thread_acceleration::ThreadAcceleration;
|
||||||
use tracing::{debug, info, trace};
|
use tracing::{debug, info, trace};
|
||||||
use tracing_log::LogTracer;
|
use tracing_log::LogTracer;
|
||||||
use tracing_subscriber::{EnvFilter, FmtSubscriber};
|
use tracing_subscriber::{EnvFilter, FmtSubscriber};
|
||||||
@ -19,6 +20,7 @@ use std::sync::{Arc, Mutex};
|
|||||||
mod audit_transaction;
|
mod audit_transaction;
|
||||||
mod gbt;
|
mod gbt;
|
||||||
mod thread_transaction;
|
mod thread_transaction;
|
||||||
|
mod thread_acceleration;
|
||||||
mod u32_hasher_types;
|
mod u32_hasher_types;
|
||||||
|
|
||||||
use u32_hasher_types::{u32hashmap_with_capacity, U32HasherState};
|
use u32_hasher_types::{u32hashmap_with_capacity, U32HasherState};
|
||||||
@ -74,10 +76,11 @@ impl GbtGenerator {
|
|||||||
///
|
///
|
||||||
/// Rejects if the thread panics or if the Mutex is poisoned.
|
/// Rejects if the thread panics or if the Mutex is poisoned.
|
||||||
#[napi]
|
#[napi]
|
||||||
pub async fn make(&self, mempool: Vec<ThreadTransaction>, max_uid: u32) -> Result<GbtResult> {
|
pub async fn make(&self, mempool: Vec<ThreadTransaction>, accelerations: Vec<ThreadAcceleration>, max_uid: u32) -> Result<GbtResult> {
|
||||||
trace!("make: Current State {:#?}", self.thread_transactions);
|
trace!("make: Current State {:#?}", self.thread_transactions);
|
||||||
run_task(
|
run_task(
|
||||||
Arc::clone(&self.thread_transactions),
|
Arc::clone(&self.thread_transactions),
|
||||||
|
accelerations,
|
||||||
max_uid as usize,
|
max_uid as usize,
|
||||||
move |map| {
|
move |map| {
|
||||||
for tx in mempool {
|
for tx in mempool {
|
||||||
@ -96,11 +99,13 @@ impl GbtGenerator {
|
|||||||
&self,
|
&self,
|
||||||
new_txs: Vec<ThreadTransaction>,
|
new_txs: Vec<ThreadTransaction>,
|
||||||
remove_txs: Vec<u32>,
|
remove_txs: Vec<u32>,
|
||||||
|
accelerations: Vec<ThreadAcceleration>,
|
||||||
max_uid: u32,
|
max_uid: u32,
|
||||||
) -> Result<GbtResult> {
|
) -> Result<GbtResult> {
|
||||||
trace!("update: Current State {:#?}", self.thread_transactions);
|
trace!("update: Current State {:#?}", self.thread_transactions);
|
||||||
run_task(
|
run_task(
|
||||||
Arc::clone(&self.thread_transactions),
|
Arc::clone(&self.thread_transactions),
|
||||||
|
accelerations,
|
||||||
max_uid as usize,
|
max_uid as usize,
|
||||||
move |map| {
|
move |map| {
|
||||||
for tx in new_txs {
|
for tx in new_txs {
|
||||||
@ -141,6 +146,7 @@ pub struct GbtResult {
|
|||||||
/// to the `HashMap` as the only argument. (A move closure is recommended to meet the bounds)
|
/// to the `HashMap` as the only argument. (A move closure is recommended to meet the bounds)
|
||||||
async fn run_task<F>(
|
async fn run_task<F>(
|
||||||
thread_transactions: Arc<Mutex<ThreadTransactionsMap>>,
|
thread_transactions: Arc<Mutex<ThreadTransactionsMap>>,
|
||||||
|
accelerations: Vec<ThreadAcceleration>,
|
||||||
max_uid: usize,
|
max_uid: usize,
|
||||||
callback: F,
|
callback: F,
|
||||||
) -> Result<GbtResult>
|
) -> Result<GbtResult>
|
||||||
@ -159,7 +165,7 @@ where
|
|||||||
callback(&mut map);
|
callback(&mut map);
|
||||||
|
|
||||||
info!("Starting gbt algorithm for {} elements...", map.len());
|
info!("Starting gbt algorithm for {} elements...", map.len());
|
||||||
let result = gbt::gbt(&mut map, max_uid);
|
let result = gbt::gbt(&mut map, &accelerations, max_uid);
|
||||||
info!("Finished gbt algorithm for {} elements...", map.len());
|
info!("Finished gbt algorithm for {} elements...", map.len());
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
|
8
backend/rust-gbt/src/thread_acceleration.rs
Normal file
8
backend/rust-gbt/src/thread_acceleration.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
use napi_derive::napi;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct ThreadAcceleration {
|
||||||
|
pub uid: u32,
|
||||||
|
pub delta: f64, // fee delta
|
||||||
|
}
|
@ -129,6 +129,10 @@
|
|||||||
"AUDIT_START_HEIGHT": 774000,
|
"AUDIT_START_HEIGHT": 774000,
|
||||||
"SERVERS": []
|
"SERVERS": []
|
||||||
},
|
},
|
||||||
|
"MEMPOOL_SERVICES": {
|
||||||
|
"API": "",
|
||||||
|
"ACCELERATIONS": false
|
||||||
|
},
|
||||||
"REDIS": {
|
"REDIS": {
|
||||||
"ENABLED": false,
|
"ENABLED": false,
|
||||||
"UNIX_SOCKET_PATH": "/tmp/redis.sock"
|
"UNIX_SOCKET_PATH": "/tmp/redis.sock"
|
||||||
|
@ -129,6 +129,11 @@ describe('Mempool Backend Config', () => {
|
|||||||
SERVERS: []
|
SERVERS: []
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(config.MEMPOOL_SERVICES).toStrictEqual({
|
||||||
|
API: "",
|
||||||
|
ACCELERATIONS: false,
|
||||||
|
});
|
||||||
|
|
||||||
expect(config.REDIS).toStrictEqual({
|
expect(config.REDIS).toStrictEqual({
|
||||||
ENABLED: false,
|
ENABLED: false,
|
||||||
UNIX_SOCKET_PATH: ''
|
UNIX_SOCKET_PATH: ''
|
||||||
@ -167,6 +172,8 @@ describe('Mempool Backend Config', () => {
|
|||||||
|
|
||||||
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
|
expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
|
||||||
|
|
||||||
|
expect(config.MEMPOOL_SERVICES).toStrictEqual(fixture.MEMPOOL_SERVICES);
|
||||||
|
|
||||||
expect(config.REDIS).toStrictEqual(fixture.REDIS);
|
expect(config.REDIS).toStrictEqual(fixture.REDIS);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -15,7 +15,7 @@ describe('Rust GBT', () => {
|
|||||||
test('should produce the same template as getBlockTemplate from Bitcoin Core', async () => {
|
test('should produce the same template as getBlockTemplate from Bitcoin Core', async () => {
|
||||||
const rustGbt = new GbtGenerator();
|
const rustGbt = new GbtGenerator();
|
||||||
const { mempool, maxUid } = mempoolFromArrayBuffer(vectorBuffer.buffer);
|
const { mempool, maxUid } = mempoolFromArrayBuffer(vectorBuffer.buffer);
|
||||||
const result = await rustGbt.make(mempool, maxUid);
|
const result = await rustGbt.make(mempool, [], maxUid);
|
||||||
|
|
||||||
const blocks: [string, number][][] = result.blocks.map(block => {
|
const blocks: [string, number][][] = result.blocks.map(block => {
|
||||||
return block.map(uid => [vectorUidMap.get(uid) || 'missing', uid]);
|
return block.map(uid => [vectorUidMap.get(uid) || 'missing', uid]);
|
||||||
|
@ -6,16 +6,17 @@ import rbfCache from './rbf-cache';
|
|||||||
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
const PROPAGATION_MARGIN = 180; // in seconds, time since a transaction is first seen after which it is assumed to have propagated to all miners
|
||||||
|
|
||||||
class Audit {
|
class Audit {
|
||||||
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended })
|
auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
|
||||||
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], score: number, similarity: number } {
|
: { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
|
||||||
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
if (!projectedBlocks?.[0]?.transactionIds || !mempool) {
|
||||||
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], score: 0, similarity: 1 };
|
return { censored: [], added: [], fresh: [], sigop: [], fullrbf: [], accelerated: [], score: 0, similarity: 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const matches: string[] = []; // present in both mined block and template
|
const matches: string[] = []; // present in both mined block and template
|
||||||
const added: string[] = []; // present in mined block, not in template
|
const added: string[] = []; // present in mined block, not in template
|
||||||
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
|
||||||
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
|
||||||
|
const accelerated: string[] = []; // prioritized by the mempool accelerator
|
||||||
const isCensored = {}; // missing, without excuse
|
const isCensored = {}; // missing, without excuse
|
||||||
const isDisplaced = {};
|
const isDisplaced = {};
|
||||||
let displacedWeight = 0;
|
let displacedWeight = 0;
|
||||||
@ -28,6 +29,9 @@ class Audit {
|
|||||||
const now = Math.round((Date.now() / 1000));
|
const now = Math.round((Date.now() / 1000));
|
||||||
for (const tx of transactions) {
|
for (const tx of transactions) {
|
||||||
inBlock[tx.txid] = tx;
|
inBlock[tx.txid] = tx;
|
||||||
|
if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
|
||||||
|
accelerated.push(tx.txid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// coinbase is always expected
|
// coinbase is always expected
|
||||||
if (transactions[0]) {
|
if (transactions[0]) {
|
||||||
@ -149,6 +153,7 @@ class Audit {
|
|||||||
fresh,
|
fresh,
|
||||||
sigop: [],
|
sigop: [],
|
||||||
fullrbf: rbf,
|
fullrbf: rbf,
|
||||||
|
accelerated,
|
||||||
score,
|
score,
|
||||||
similarity,
|
similarity,
|
||||||
};
|
};
|
||||||
|
@ -214,6 +214,7 @@ class BitcoinRoutes {
|
|||||||
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
|
effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
|
||||||
sigops: tx.sigops,
|
sigops: tx.sigops,
|
||||||
adjustedVsize: tx.adjustedVsize,
|
adjustedVsize: tx.adjustedVsize,
|
||||||
|
acceleration: tx.acceleration
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -111,6 +111,7 @@ export class Common {
|
|||||||
fee: tx.fee || 0,
|
fee: tx.fee || 0,
|
||||||
vsize: tx.weight / 4,
|
vsize: tx.weight / 4,
|
||||||
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
|
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
|
||||||
|
acc: tx.acceleration || undefined,
|
||||||
rate: tx.effectiveFeePerVsize,
|
rate: tx.effectiveFeePerVsize,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -460,7 +461,7 @@ export class Common {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string }[]): EffectiveFeeStats {
|
static calcEffectiveFeeStatistics(transactions: { weight: number, fee: number, effectiveFeePerVsize?: number, txid: string, acceleration?: boolean }[]): EffectiveFeeStats {
|
||||||
const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate);
|
const sortedTxs = transactions.map(tx => { return { txid: tx.txid, weight: tx.weight, rate: tx.effectiveFeePerVsize || ((tx.fee || 0) / (tx.weight / 4)) }; }).sort((a, b) => a.rate - b.rate);
|
||||||
|
|
||||||
let weightCount = 0;
|
let weightCount = 0;
|
||||||
|
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
|
|||||||
import { RowDataPacket } from 'mysql2';
|
import { RowDataPacket } from 'mysql2';
|
||||||
|
|
||||||
class DatabaseMigration {
|
class DatabaseMigration {
|
||||||
private static currentVersion = 64;
|
private static currentVersion = 65;
|
||||||
private queryTimeout = 3600_000;
|
private queryTimeout = 3600_000;
|
||||||
private statisticsAddedIndexed = false;
|
private statisticsAddedIndexed = false;
|
||||||
private uniqueLogs: string[] = [];
|
private uniqueLogs: string[] = [];
|
||||||
@ -548,6 +548,11 @@ class DatabaseMigration {
|
|||||||
await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
|
await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
|
||||||
await this.updateToSchemaVersion(64);
|
await this.updateToSchemaVersion(64);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (databaseSchemaVersion < 65 && isBitcoin === true) {
|
||||||
|
await this.$executeQuery('ALTER TABLE `blocks_audits` ADD accelerated_txs JSON DEFAULT "[]"');
|
||||||
|
await this.updateToSchemaVersion(65);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction } from '../../rust-gbt';
|
import { GbtGenerator, GbtResult, ThreadTransaction as RustThreadTransaction, ThreadAcceleration as RustThreadAcceleration } from '../../rust-gbt';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { MempoolBlock, MempoolTransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats } from '../mempool.interfaces';
|
import { MempoolBlock, MempoolTransactionExtended, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction, EffectiveFeeStats, PoolTag } from '../mempool.interfaces';
|
||||||
import { Common, OnlineFeeStatsCalculator } from './common';
|
import { Common, OnlineFeeStatsCalculator } from './common';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { Worker } from 'worker_threads';
|
import { Worker } from 'worker_threads';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import mempool from './mempool';
|
||||||
|
|
||||||
const MAX_UINT32 = Math.pow(2, 32) - 1;
|
const MAX_UINT32 = Math.pow(2, 32) - 1;
|
||||||
|
|
||||||
@ -170,7 +171,7 @@ class MempoolBlocks {
|
|||||||
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
|
for (let i = 0; i < Math.max(mempoolBlocks.length, prevBlocks.length); i++) {
|
||||||
let added: TransactionStripped[] = [];
|
let added: TransactionStripped[] = [];
|
||||||
let removed: string[] = [];
|
let removed: string[] = [];
|
||||||
const changed: { txid: string, rate: number | undefined }[] = [];
|
const changed: { txid: string, rate: number | undefined, acc: boolean | undefined }[] = [];
|
||||||
if (mempoolBlocks[i] && !prevBlocks[i]) {
|
if (mempoolBlocks[i] && !prevBlocks[i]) {
|
||||||
added = mempoolBlocks[i].transactions;
|
added = mempoolBlocks[i].transactions;
|
||||||
} else if (!mempoolBlocks[i] && prevBlocks[i]) {
|
} else if (!mempoolBlocks[i] && prevBlocks[i]) {
|
||||||
@ -192,8 +193,8 @@ class MempoolBlocks {
|
|||||||
mempoolBlocks[i].transactions.forEach(tx => {
|
mempoolBlocks[i].transactions.forEach(tx => {
|
||||||
if (!prevIds[tx.txid]) {
|
if (!prevIds[tx.txid]) {
|
||||||
added.push(tx);
|
added.push(tx);
|
||||||
} else if (tx.rate !== prevIds[tx.txid].rate) {
|
} else if (tx.rate !== prevIds[tx.txid].rate || tx.acc !== prevIds[tx.txid].acc) {
|
||||||
changed.push({ txid: tx.txid, rate: tx.rate });
|
changed.push({ txid: tx.txid, rate: tx.rate, acc: tx.acc });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -206,14 +207,19 @@ class MempoolBlocks {
|
|||||||
return mempoolBlockDeltas;
|
return mempoolBlockDeltas;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
|
public async $makeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
// reset mempool short ids
|
// reset mempool short ids
|
||||||
this.resetUids();
|
if (saveResults) {
|
||||||
for (const tx of Object.values(newMempool)) {
|
this.resetUids();
|
||||||
this.setUid(tx);
|
|
||||||
}
|
}
|
||||||
|
// set missing short ids
|
||||||
|
for (const tx of Object.values(newMempool)) {
|
||||||
|
this.setUid(tx, !saveResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
|
||||||
|
|
||||||
// prepare a stripped down version of the mempool with only the minimum necessary data
|
// prepare a stripped down version of the mempool with only the minimum necessary data
|
||||||
// to reduce the overhead of passing this data to the worker thread
|
// to reduce the overhead of passing this data to the worker thread
|
||||||
@ -222,7 +228,7 @@ class MempoolBlocks {
|
|||||||
if (entry.uid !== null && entry.uid !== undefined) {
|
if (entry.uid !== null && entry.uid !== undefined) {
|
||||||
const stripped = {
|
const stripped = {
|
||||||
uid: entry.uid,
|
uid: entry.uid,
|
||||||
fee: entry.fee,
|
fee: entry.fee + (useAccelerations && (!accelerationPool || accelerations[entry.txid]?.pools?.includes(accelerationPool)) ? (accelerations[entry.txid]?.feeDelta || 0) : 0),
|
||||||
weight: (entry.adjustedVsize * 4),
|
weight: (entry.adjustedVsize * 4),
|
||||||
sigops: entry.sigops,
|
sigops: entry.sigops,
|
||||||
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
|
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
|
||||||
@ -262,7 +268,7 @@ class MempoolBlocks {
|
|||||||
// clean up thread error listener
|
// clean up thread error listener
|
||||||
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
||||||
|
|
||||||
const processed = this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), saveResults);
|
const processed = this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), accelerations, accelerationPool, saveResults);
|
||||||
|
|
||||||
logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
|
logger.debug(`makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
|
||||||
|
|
||||||
@ -273,25 +279,29 @@ class MempoolBlocks {
|
|||||||
return this.mempoolBlocks;
|
return this.mempoolBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $updateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], saveResults: boolean = false): Promise<void> {
|
public async $updateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], accelerationDelta: string[] = [], saveResults: boolean = false, useAccelerations: boolean = false): Promise<void> {
|
||||||
if (!this.txSelectionWorker) {
|
if (!this.txSelectionWorker) {
|
||||||
// need to reset the worker
|
// need to reset the worker
|
||||||
await this.$makeBlockTemplates(newMempool, saveResults);
|
await this.$makeBlockTemplates(newMempool, saveResults, useAccelerations);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
for (const tx of Object.values(added)) {
|
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
|
||||||
|
const addedAndChanged: MempoolTransactionExtended[] = useAccelerations ? accelerationDelta.map(txid => newMempool[txid]).filter(tx => tx != null).concat(added) : added;
|
||||||
|
|
||||||
|
for (const tx of addedAndChanged) {
|
||||||
this.setUid(tx, true);
|
this.setUid(tx, true);
|
||||||
}
|
}
|
||||||
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => (uid !== null && uid !== undefined)) as number[];
|
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[];
|
||||||
|
|
||||||
// prepare a stripped down version of the mempool with only the minimum necessary data
|
// prepare a stripped down version of the mempool with only the minimum necessary data
|
||||||
// to reduce the overhead of passing this data to the worker thread
|
// to reduce the overhead of passing this data to the worker thread
|
||||||
const addedStripped: CompactThreadTransaction[] = added.filter(entry => (entry.uid !== null && entry.uid !== undefined)).map(entry => {
|
const addedStripped: CompactThreadTransaction[] = addedAndChanged.filter(entry => entry.uid != null).map(entry => {
|
||||||
return {
|
return {
|
||||||
uid: entry.uid || 0,
|
uid: entry.uid || 0,
|
||||||
fee: entry.fee,
|
fee: entry.fee + (useAccelerations ? (accelerations[entry.txid]?.feeDelta || 0) : 0),
|
||||||
weight: (entry.adjustedVsize * 4),
|
weight: (entry.adjustedVsize * 4),
|
||||||
sigops: entry.sigops,
|
sigops: entry.sigops,
|
||||||
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
|
feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
|
||||||
@ -318,7 +328,7 @@ class MempoolBlocks {
|
|||||||
// clean up thread error listener
|
// clean up thread error listener
|
||||||
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
this.txSelectionWorker?.removeListener('error', threadErrorListener);
|
||||||
|
|
||||||
this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), saveResults);
|
this.processBlockTemplates(newMempool, blocks, null, Object.entries(rates), Object.values(clusters), accelerations, null, saveResults);
|
||||||
logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
|
logger.debug(`updateBlockTemplates completed in ${(Date.now() - start) / 1000} seconds`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
||||||
@ -330,7 +340,8 @@ class MempoolBlocks {
|
|||||||
this.rustGbtGenerator = new GbtGenerator();
|
this.rustGbtGenerator = new GbtGenerator();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $rustMakeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
|
public async $rustMakeBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, saveResults: boolean = false, useAccelerations: boolean = false, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
|
||||||
|
console.log('$rustMakeBlockTemplates');
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
// reset mempool short ids
|
// reset mempool short ids
|
||||||
@ -346,16 +357,25 @@ class MempoolBlocks {
|
|||||||
tx.inputs = tx.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => (uid !== null && uid !== undefined)) as number[];
|
tx.inputs = tx.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => (uid !== null && uid !== undefined)) as number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
|
||||||
|
const acceleratedList = accelerationPool ? Object.values(accelerations).filter(acc => newMempool[acc.txid] && acc.pools.includes(accelerationPool)) : Object.values(accelerations).filter(acc => newMempool[acc.txid]);
|
||||||
|
const convertedAccelerations = acceleratedList.map(acc => {
|
||||||
|
return {
|
||||||
|
uid: this.getUid(newMempool[acc.txid]),
|
||||||
|
delta: acc.feeDelta,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// run the block construction algorithm in a separate thread, and wait for a result
|
// run the block construction algorithm in a separate thread, and wait for a result
|
||||||
const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator();
|
const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator();
|
||||||
try {
|
try {
|
||||||
const { blocks, blockWeights, rates, clusters } = this.convertNapiResultTxids(
|
const { blocks, blockWeights, rates, clusters } = this.convertNapiResultTxids(
|
||||||
await rustGbt.make(Object.values(newMempool) as RustThreadTransaction[], this.nextUid),
|
await rustGbt.make(Object.values(newMempool) as RustThreadTransaction[], convertedAccelerations as RustThreadAcceleration[], this.nextUid),
|
||||||
);
|
);
|
||||||
if (saveResults) {
|
if (saveResults) {
|
||||||
this.rustInitialized = true;
|
this.rustInitialized = true;
|
||||||
}
|
}
|
||||||
const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, saveResults);
|
const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, accelerations, accelerationPool, saveResults);
|
||||||
logger.debug(`RUST makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
|
logger.debug(`RUST makeBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
|
||||||
return processed;
|
return processed;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -367,20 +387,20 @@ class MempoolBlocks {
|
|||||||
return this.mempoolBlocks;
|
return this.mempoolBlocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $oneOffRustBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }): Promise<MempoolBlockWithTransactions[]> {
|
public async $oneOffRustBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, useAccelerations: boolean, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
|
||||||
return this.$rustMakeBlockTemplates(newMempool, false);
|
return this.$rustMakeBlockTemplates(newMempool, false, useAccelerations, accelerationPool);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $rustUpdateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[]): Promise<void> {
|
public async $rustUpdateBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number, added: MempoolTransactionExtended[], removed: MempoolTransactionExtended[], useAccelerations: boolean, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
|
||||||
// GBT optimization requires that uids never get too sparse
|
// GBT optimization requires that uids never get too sparse
|
||||||
// as a sanity check, we should also explicitly prevent uint32 uid overflow
|
// as a sanity check, we should also explicitly prevent uint32 uid overflow
|
||||||
if (this.nextUid + added.length >= Math.min(Math.max(262144, 2 * mempoolSize), MAX_UINT32)) {
|
if (this.nextUid + added.length >= Math.min(Math.max(262144, 2 * mempoolSize), MAX_UINT32)) {
|
||||||
this.resetRustGbt();
|
this.resetRustGbt();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.rustInitialized) {
|
if (!this.rustInitialized) {
|
||||||
// need to reset the worker
|
// need to reset the worker
|
||||||
await this.$rustMakeBlockTemplates(newMempool, true);
|
return this.$rustMakeBlockTemplates(newMempool, true, useAccelerations, accelerationPool);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
@ -394,12 +414,22 @@ class MempoolBlocks {
|
|||||||
}
|
}
|
||||||
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => (uid !== null && uid !== undefined)) as number[];
|
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => (uid !== null && uid !== undefined)) as number[];
|
||||||
|
|
||||||
|
const accelerations = useAccelerations ? mempool.getAccelerations() : {};
|
||||||
|
const acceleratedList = accelerationPool ? Object.values(accelerations).filter(acc => newMempool[acc.txid] && acc.pools.includes(accelerationPool)) : Object.values(accelerations).filter(acc => newMempool[acc.txid]);
|
||||||
|
const convertedAccelerations = acceleratedList.map(acc => {
|
||||||
|
return {
|
||||||
|
uid: this.getUid(newMempool[acc.txid]),
|
||||||
|
delta: acc.feeDelta,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// run the block construction algorithm in a separate thread, and wait for a result
|
// run the block construction algorithm in a separate thread, and wait for a result
|
||||||
try {
|
try {
|
||||||
const { blocks, blockWeights, rates, clusters } = this.convertNapiResultTxids(
|
const { blocks, blockWeights, rates, clusters } = this.convertNapiResultTxids(
|
||||||
await this.rustGbtGenerator.update(
|
await this.rustGbtGenerator.update(
|
||||||
added as RustThreadTransaction[],
|
added as RustThreadTransaction[],
|
||||||
removedUids,
|
removedUids,
|
||||||
|
convertedAccelerations as RustThreadAcceleration[],
|
||||||
this.nextUid,
|
this.nextUid,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -407,17 +437,19 @@ class MempoolBlocks {
|
|||||||
if (mempoolSize !== resultMempoolSize) {
|
if (mempoolSize !== resultMempoolSize) {
|
||||||
throw new Error('GBT returned wrong number of transactions, cache is probably out of sync');
|
throw new Error('GBT returned wrong number of transactions, cache is probably out of sync');
|
||||||
} else {
|
} else {
|
||||||
this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, true);
|
const processed = this.processBlockTemplates(newMempool, blocks, blockWeights, rates, clusters, accelerations, accelerationPool, true);
|
||||||
|
this.removeUids(removedUids);
|
||||||
|
logger.debug(`RUST updateBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
|
||||||
|
return processed;
|
||||||
}
|
}
|
||||||
this.removeUids(removedUids);
|
|
||||||
logger.debug(`RUST updateBlockTemplates completed in ${(Date.now() - start)/1000} seconds`);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.err('RUST updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
logger.err('RUST updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
|
||||||
this.resetRustGbt();
|
this.resetRustGbt();
|
||||||
|
return this.mempoolBlocks;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], saveResults): MempoolBlockWithTransactions[] {
|
private processBlockTemplates(mempool: { [txid: string]: MempoolTransactionExtended }, blocks: string[][], blockWeights: number[] | null, rates: [string, number][], clusters: string[][], accelerations, accelerationPool, saveResults): MempoolBlockWithTransactions[] {
|
||||||
for (const [txid, rate] of rates) {
|
for (const [txid, rate] of rates) {
|
||||||
if (txid in mempool) {
|
if (txid in mempool) {
|
||||||
mempool[txid].effectiveFeePerVsize = rate;
|
mempool[txid].effectiveFeePerVsize = rate;
|
||||||
@ -468,6 +500,8 @@ class MempoolBlocks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isAccelerated : { [txid: string]: boolean } = {};
|
||||||
|
|
||||||
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
const sizeLimit = (config.MEMPOOL.BLOCK_WEIGHT_UNITS / 4) * 1.2;
|
||||||
// update this thread's mempool with the results
|
// update this thread's mempool with the results
|
||||||
let mempoolTx: MempoolTransactionExtended;
|
let mempoolTx: MempoolTransactionExtended;
|
||||||
@ -496,6 +530,17 @@ class MempoolBlocks {
|
|||||||
mempoolTx.cpfpChecked = true;
|
mempoolTx.cpfpChecked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const acceleration = accelerations[txid];
|
||||||
|
if (isAccelerated[txid] || (acceleration && (!accelerationPool || acceleration.pools.includes(accelerationPool)))) {
|
||||||
|
mempoolTx.acceleration = true;
|
||||||
|
for (const ancestor of mempoolTx.ancestors || []) {
|
||||||
|
mempool[ancestor.txid].acceleration = true;
|
||||||
|
isAccelerated[ancestor.txid] = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete mempoolTx.acceleration;
|
||||||
|
}
|
||||||
|
|
||||||
// online calculation of stack-of-blocks fee stats
|
// online calculation of stack-of-blocks fee stats
|
||||||
if (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) {
|
if (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) {
|
||||||
feeStatsCalculator.processNext(mempoolTx);
|
feeStatsCalculator.processNext(mempoolTx);
|
||||||
@ -532,7 +577,7 @@ class MempoolBlocks {
|
|||||||
|
|
||||||
private dataToMempoolBlocks(transactionIds: string[], transactions: MempoolTransactionExtended[], totalSize: number, totalWeight: number, totalFees: number, feeStats?: EffectiveFeeStats ): MempoolBlockWithTransactions {
|
private dataToMempoolBlocks(transactionIds: string[], transactions: MempoolTransactionExtended[], totalSize: number, totalWeight: number, totalFees: number, feeStats?: EffectiveFeeStats ): MempoolBlockWithTransactions {
|
||||||
if (!feeStats) {
|
if (!feeStats) {
|
||||||
feeStats = Common.calcEffectiveFeeStatistics(transactions);
|
feeStats = Common.calcEffectiveFeeStatistics(transactions.filter(tx => !tx.acceleration));
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
blockSize: totalSize,
|
blockSize: totalSize,
|
||||||
|
@ -9,6 +9,7 @@ import loadingIndicators from './loading-indicators';
|
|||||||
import bitcoinClient from './bitcoin/bitcoin-client';
|
import bitcoinClient from './bitcoin/bitcoin-client';
|
||||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
|
||||||
import rbfCache from './rbf-cache';
|
import rbfCache from './rbf-cache';
|
||||||
|
import accelerationApi, { Acceleration } from './services/acceleration';
|
||||||
import redisCache from './redis-cache';
|
import redisCache from './redis-cache';
|
||||||
|
|
||||||
class Mempool {
|
class Mempool {
|
||||||
@ -19,9 +20,11 @@ class Mempool {
|
|||||||
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
|
||||||
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
|
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
|
||||||
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
private mempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, newTransactions: MempoolTransactionExtended[],
|
||||||
deletedTransactions: MempoolTransactionExtended[]) => void) | undefined;
|
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void) | undefined;
|
||||||
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
|
private $asyncMempoolChangedCallback: ((newMempool: {[txId: string]: MempoolTransactionExtended; }, mempoolSize: number, newTransactions: MempoolTransactionExtended[],
|
||||||
deletedTransactions: MempoolTransactionExtended[]) => Promise<void>) | undefined;
|
deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>) | undefined;
|
||||||
|
|
||||||
|
private accelerations: { [txId: string]: Acceleration } = {};
|
||||||
|
|
||||||
private txPerSecondArray: number[] = [];
|
private txPerSecondArray: number[] = [];
|
||||||
private txPerSecond: number = 0;
|
private txPerSecond: number = 0;
|
||||||
@ -66,12 +69,12 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => void): void {
|
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
|
||||||
this.mempoolChangedCallback = fn;
|
this.mempoolChangedCallback = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
|
public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => Promise<void>): void {
|
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>): void {
|
||||||
this.$asyncMempoolChangedCallback = fn;
|
this.$asyncMempoolChangedCallback = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,10 +110,10 @@ class Mempool {
|
|||||||
logger.debug(`Finished migrating cache transactions in ${((Date.now() - redisTimer) / 1000).toFixed(2)} seconds`);
|
logger.debug(`Finished migrating cache transactions in ${((Date.now() - redisTimer) / 1000).toFixed(2)} seconds`);
|
||||||
}
|
}
|
||||||
if (this.mempoolChangedCallback) {
|
if (this.mempoolChangedCallback) {
|
||||||
this.mempoolChangedCallback(this.mempoolCache, [], []);
|
this.mempoolChangedCallback(this.mempoolCache, [], [], []);
|
||||||
}
|
}
|
||||||
if (this.$asyncMempoolChangedCallback) {
|
if (this.$asyncMempoolChangedCallback) {
|
||||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, count, [], []);
|
await this.$asyncMempoolChangedCallback(this.mempoolCache, count, [], [], []);
|
||||||
}
|
}
|
||||||
this.addToSpendMap(Object.values(this.mempoolCache));
|
this.addToSpendMap(Object.values(this.mempoolCache));
|
||||||
}
|
}
|
||||||
@ -321,14 +324,19 @@ class Mempool {
|
|||||||
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
|
||||||
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
|
this.latestTransactions = newTransactionsStripped.concat(this.latestTransactions).slice(0, 6);
|
||||||
|
|
||||||
|
const accelerationDelta = await this.$updateAccelerations();
|
||||||
|
if (accelerationDelta.length) {
|
||||||
|
hasChange = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.mempoolCacheDelta = Math.abs(transactions.length - newMempoolSize);
|
this.mempoolCacheDelta = Math.abs(transactions.length - newMempoolSize);
|
||||||
|
|
||||||
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
if (this.mempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||||
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions);
|
this.mempoolChangedCallback(this.mempoolCache, newTransactions, deletedTransactions, accelerationDelta);
|
||||||
}
|
}
|
||||||
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
if (this.$asyncMempoolChangedCallback && (hasChange || deletedTransactions.length)) {
|
||||||
this.updateTimerProgress(timer, 'running async mempool callback');
|
this.updateTimerProgress(timer, 'running async mempool callback');
|
||||||
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions);
|
await this.$asyncMempoolChangedCallback(this.mempoolCache, newMempoolSize, newTransactions, deletedTransactions, accelerationDelta);
|
||||||
this.updateTimerProgress(timer, 'completed async mempool callback');
|
this.updateTimerProgress(timer, 'completed async mempool callback');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,6 +360,70 @@ class Mempool {
|
|||||||
this.clearTimer(timer);
|
this.clearTimer(timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getAccelerations(): { [txid: string]: Acceleration } {
|
||||||
|
return this.accelerations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async $updateAccelerations(): Promise<string[]> {
|
||||||
|
if (!config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newAccelerations = await accelerationApi.$fetchAccelerations();
|
||||||
|
|
||||||
|
const changed: string[] = [];
|
||||||
|
|
||||||
|
const newAccelerationMap: { [txid: string]: Acceleration } = {};
|
||||||
|
for (const acceleration of newAccelerations) {
|
||||||
|
newAccelerationMap[acceleration.txid] = acceleration;
|
||||||
|
if (this.accelerations[acceleration.txid] == null) {
|
||||||
|
// new acceleration
|
||||||
|
changed.push(acceleration.txid);
|
||||||
|
} else {
|
||||||
|
if (this.accelerations[acceleration.txid].feeDelta !== acceleration.feeDelta) {
|
||||||
|
// feeDelta changed
|
||||||
|
changed.push(acceleration.txid);
|
||||||
|
} else if (this.accelerations[acceleration.txid].pools?.length) {
|
||||||
|
let poolsChanged = false;
|
||||||
|
const pools = new Set();
|
||||||
|
this.accelerations[acceleration.txid].pools.forEach(pool => {
|
||||||
|
pools.add(pool);
|
||||||
|
});
|
||||||
|
acceleration.pools.forEach(pool => {
|
||||||
|
if (!pools.has(pool)) {
|
||||||
|
poolsChanged = true;
|
||||||
|
} else {
|
||||||
|
pools.delete(pool);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (pools.size > 0) {
|
||||||
|
poolsChanged = true;
|
||||||
|
}
|
||||||
|
if (poolsChanged) {
|
||||||
|
// pools changed
|
||||||
|
changed.push(acceleration.txid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const oldTxid of Object.keys(this.accelerations)) {
|
||||||
|
if (!newAccelerationMap[oldTxid]) {
|
||||||
|
// removed
|
||||||
|
changed.push(oldTxid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.accelerations = newAccelerationMap;
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.debug(`Failed to update accelerations: ` + (e instanceof Error ? e.message : e));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private startTimer() {
|
private startTimer() {
|
||||||
const state: any = {
|
const state: any = {
|
||||||
start: Date.now(),
|
start: Date.now(),
|
||||||
|
@ -107,6 +107,7 @@ class Mining {
|
|||||||
slug: poolInfo.slug,
|
slug: poolInfo.slug,
|
||||||
avgMatchRate: poolInfo.avgMatchRate !== null ? Math.round(100 * poolInfo.avgMatchRate) / 100 : null,
|
avgMatchRate: poolInfo.avgMatchRate !== null ? Math.round(100 * poolInfo.avgMatchRate) / 100 : null,
|
||||||
avgFeeDelta: poolInfo.avgFeeDelta,
|
avgFeeDelta: poolInfo.avgFeeDelta,
|
||||||
|
poolUniqueId: poolInfo.poolUniqueId
|
||||||
};
|
};
|
||||||
poolsStats.push(poolStat);
|
poolsStats.push(poolStat);
|
||||||
});
|
});
|
||||||
|
30
backend/src/api/services/acceleration.ts
Normal file
30
backend/src/api/services/acceleration.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { query } from '../../utils/axios-query';
|
||||||
|
import config from '../../config';
|
||||||
|
import { BlockExtended, PoolTag } from '../../mempool.interfaces';
|
||||||
|
|
||||||
|
export interface Acceleration {
|
||||||
|
txid: string,
|
||||||
|
feeDelta: number,
|
||||||
|
pools: number[],
|
||||||
|
}
|
||||||
|
|
||||||
|
class AccelerationApi {
|
||||||
|
public async $fetchAccelerations(): Promise<Acceleration[]> {
|
||||||
|
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
|
||||||
|
const response = await query(`${config.MEMPOOL_SERVICES.API}/accelerator/accelerations`);
|
||||||
|
return (response as Acceleration[]) || [];
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isAcceleratedBlock(block: BlockExtended, accelerations: Acceleration[]): boolean {
|
||||||
|
let anyAccelerated = false;
|
||||||
|
for (let i = 0; i < accelerations.length && !anyAccelerated; i++) {
|
||||||
|
anyAccelerated = anyAccelerated || accelerations[i].pools?.includes(block.extras.pool.id);
|
||||||
|
}
|
||||||
|
return anyAccelerated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AccelerationApi();
|
@ -21,6 +21,8 @@ import Audit from './audit';
|
|||||||
import { deepClone } from '../utils/clone';
|
import { deepClone } from '../utils/clone';
|
||||||
import priceUpdater from '../tasks/price-updater';
|
import priceUpdater from '../tasks/price-updater';
|
||||||
import { ApiPrice } from '../repositories/PricesRepository';
|
import { ApiPrice } from '../repositories/PricesRepository';
|
||||||
|
import accelerationApi from './services/acceleration';
|
||||||
|
import mempool from './mempool';
|
||||||
|
|
||||||
// valid 'want' subscriptions
|
// valid 'want' subscriptions
|
||||||
const wantable = [
|
const wantable = [
|
||||||
@ -172,9 +174,15 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
const tx = memPool.getMempool()[trackTxid];
|
const tx = memPool.getMempool()[trackTxid];
|
||||||
if (tx && tx.position) {
|
if (tx && tx.position) {
|
||||||
|
const position: { block: number, vsize: number, accelerated?: boolean } = {
|
||||||
|
...tx.position
|
||||||
|
};
|
||||||
|
if (tx.acceleration) {
|
||||||
|
position.accelerated = tx.acceleration;
|
||||||
|
}
|
||||||
response['txPosition'] = JSON.stringify({
|
response['txPosition'] = JSON.stringify({
|
||||||
txid: trackTxid,
|
txid: trackTxid,
|
||||||
position: tx.position,
|
position
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -390,7 +398,7 @@ class WebsocketHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
|
async $handleMempoolChange(newMempool: { [txid: string]: MempoolTransactionExtended }, mempoolSize: number,
|
||||||
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]): Promise<void> {
|
newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]): Promise<void> {
|
||||||
if (!this.wss) {
|
if (!this.wss) {
|
||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
@ -399,9 +407,9 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||||
if (config.MEMPOOL.RUST_GBT) {
|
if (config.MEMPOOL.RUST_GBT) {
|
||||||
await mempoolBlocks.$rustUpdateBlockTemplates(newMempool, mempoolSize, newTransactions, deletedTransactions);
|
await mempoolBlocks.$rustUpdateBlockTemplates(newMempool, mempoolSize, newTransactions, deletedTransactions, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||||
} else {
|
} else {
|
||||||
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true);
|
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mempoolBlocks.updateMempoolBlocks(newMempool, true);
|
mempoolBlocks.updateMempoolBlocks(newMempool, true);
|
||||||
@ -647,7 +655,10 @@ class WebsocketHandler {
|
|||||||
if (mempoolTx && mempoolTx.position) {
|
if (mempoolTx && mempoolTx.position) {
|
||||||
response['txPosition'] = JSON.stringify({
|
response['txPosition'] = JSON.stringify({
|
||||||
txid: trackTxid,
|
txid: trackTxid,
|
||||||
position: mempoolTx.position,
|
position: {
|
||||||
|
...mempoolTx.position,
|
||||||
|
accelerated: mempoolTx.acceleration || undefined,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -695,6 +706,7 @@ class WebsocketHandler {
|
|||||||
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
|
||||||
let projectedBlocks;
|
let projectedBlocks;
|
||||||
let auditMempool = _memPool;
|
let auditMempool = _memPool;
|
||||||
|
const isAccelerated = config.MEMPOOL_SERVICES.ACCELERATIONS && accelerationApi.isAcceleratedBlock(block, Object.values(mempool.getAccelerations()));
|
||||||
// template calculation functions have mempool side effects, so calculate audits using
|
// template calculation functions have mempool side effects, so calculate audits using
|
||||||
// a cloned copy of the mempool if we're running a different algorithm for mempool updates
|
// a cloned copy of the mempool if we're running a different algorithm for mempool updates
|
||||||
const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL;
|
const separateAudit = config.MEMPOOL.ADVANCED_GBT_AUDIT !== config.MEMPOOL.ADVANCED_GBT_MEMPOOL;
|
||||||
@ -702,19 +714,27 @@ class WebsocketHandler {
|
|||||||
auditMempool = deepClone(_memPool);
|
auditMempool = deepClone(_memPool);
|
||||||
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
|
if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
|
||||||
if (config.MEMPOOL.RUST_GBT) {
|
if (config.MEMPOOL.RUST_GBT) {
|
||||||
projectedBlocks = await mempoolBlocks.$oneOffRustBlockTemplates(auditMempool);
|
projectedBlocks = await mempoolBlocks.$oneOffRustBlockTemplates(auditMempool, isAccelerated, block.extras.pool.id);
|
||||||
} else {
|
} else {
|
||||||
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false);
|
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated, block.extras.pool.id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
|
projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
if ((config.MEMPOOL_SERVICES.ACCELERATIONS)) {
|
||||||
|
if (config.MEMPOOL.RUST_GBT) {
|
||||||
|
projectedBlocks = await mempoolBlocks.$rustUpdateBlockTemplates(auditMempool, Object.keys(auditMempool).length, [], [], isAccelerated, block.extras.pool.id);
|
||||||
|
} else {
|
||||||
|
projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated, block.extras.pool.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
projectedBlocks = mempoolBlocks.getMempoolBlocksWithTransactions();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Common.indexingEnabled()) {
|
if (Common.indexingEnabled()) {
|
||||||
const { censored, added, fresh, sigop, fullrbf, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
const { censored, added, fresh, sigop, fullrbf, accelerated, score, similarity } = Audit.auditBlock(transactions, projectedBlocks, auditMempool);
|
||||||
const matchRate = Math.round(score * 100 * 100) / 100;
|
const matchRate = Math.round(score * 100 * 100) / 100;
|
||||||
|
|
||||||
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
|
const stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
|
||||||
@ -743,6 +763,7 @@ class WebsocketHandler {
|
|||||||
freshTxs: fresh,
|
freshTxs: fresh,
|
||||||
sigopTxs: sigop,
|
sigopTxs: sigop,
|
||||||
fullrbfTxs: fullrbf,
|
fullrbfTxs: fullrbf,
|
||||||
|
acceleratedTxs: accelerated,
|
||||||
matchRate: matchRate,
|
matchRate: matchRate,
|
||||||
expectedFees: totalFees,
|
expectedFees: totalFees,
|
||||||
expectedWeight: totalWeight,
|
expectedWeight: totalWeight,
|
||||||
@ -770,9 +791,9 @@ class WebsocketHandler {
|
|||||||
|
|
||||||
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
|
||||||
if (config.MEMPOOL.RUST_GBT) {
|
if (config.MEMPOOL.RUST_GBT) {
|
||||||
await mempoolBlocks.$rustUpdateBlockTemplates(_memPool, Object.keys(_memPool).length, [], transactions);
|
await mempoolBlocks.$rustUpdateBlockTemplates(_memPool, Object.keys(_memPool).length, [], transactions, true);
|
||||||
} else {
|
} else {
|
||||||
await mempoolBlocks.$makeBlockTemplates(_memPool, true);
|
await mempoolBlocks.$makeBlockTemplates(_memPool, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mempoolBlocks.updateMempoolBlocks(_memPool, true);
|
mempoolBlocks.updateMempoolBlocks(_memPool, true);
|
||||||
@ -836,7 +857,10 @@ class WebsocketHandler {
|
|||||||
if (mempoolTx && mempoolTx.position) {
|
if (mempoolTx && mempoolTx.position) {
|
||||||
response['txPosition'] = JSON.stringify({
|
response['txPosition'] = JSON.stringify({
|
||||||
txid: trackTxid,
|
txid: trackTxid,
|
||||||
position: mempoolTx.position,
|
position: {
|
||||||
|
...mempoolTx.position,
|
||||||
|
accelerated: mempoolTx.acceleration || undefined,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,6 +139,10 @@ interface IConfig {
|
|||||||
AUDIT_START_HEIGHT: number;
|
AUDIT_START_HEIGHT: number;
|
||||||
SERVERS: string[];
|
SERVERS: string[];
|
||||||
},
|
},
|
||||||
|
MEMPOOL_SERVICES: {
|
||||||
|
API: string;
|
||||||
|
ACCELERATIONS: boolean;
|
||||||
|
},
|
||||||
REDIS: {
|
REDIS: {
|
||||||
ENABLED: boolean;
|
ENABLED: boolean;
|
||||||
UNIX_SOCKET_PATH: string;
|
UNIX_SOCKET_PATH: string;
|
||||||
@ -282,6 +286,10 @@ const defaults: IConfig = {
|
|||||||
'AUDIT_START_HEIGHT': 774000,
|
'AUDIT_START_HEIGHT': 774000,
|
||||||
'SERVERS': [],
|
'SERVERS': [],
|
||||||
},
|
},
|
||||||
|
'MEMPOOL_SERVICES': {
|
||||||
|
'API': '',
|
||||||
|
'ACCELERATIONS': false,
|
||||||
|
},
|
||||||
'REDIS': {
|
'REDIS': {
|
||||||
'ENABLED': false,
|
'ENABLED': false,
|
||||||
'UNIX_SOCKET_PATH': '',
|
'UNIX_SOCKET_PATH': '',
|
||||||
@ -306,6 +314,7 @@ class Config implements IConfig {
|
|||||||
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
|
||||||
MAXMIND: IConfig['MAXMIND'];
|
MAXMIND: IConfig['MAXMIND'];
|
||||||
REPLICATION: IConfig['REPLICATION'];
|
REPLICATION: IConfig['REPLICATION'];
|
||||||
|
MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
|
||||||
REDIS: IConfig['REDIS'];
|
REDIS: IConfig['REDIS'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -327,6 +336,7 @@ class Config implements IConfig {
|
|||||||
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
|
||||||
this.MAXMIND = configs.MAXMIND;
|
this.MAXMIND = configs.MAXMIND;
|
||||||
this.REPLICATION = configs.REPLICATION;
|
this.REPLICATION = configs.REPLICATION;
|
||||||
|
this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
|
||||||
this.REDIS = configs.REDIS;
|
this.REDIS = configs.REDIS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ export interface PoolInfo {
|
|||||||
slug: string;
|
slug: string;
|
||||||
avgMatchRate: number | null;
|
avgMatchRate: number | null;
|
||||||
avgFeeDelta: number | null;
|
avgFeeDelta: number | null;
|
||||||
|
poolUniqueId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PoolStats extends PoolInfo {
|
export interface PoolStats extends PoolInfo {
|
||||||
@ -36,6 +37,7 @@ export interface BlockAudit {
|
|||||||
sigopTxs: string[],
|
sigopTxs: string[],
|
||||||
fullrbfTxs: string[],
|
fullrbfTxs: string[],
|
||||||
addedTxs: string[],
|
addedTxs: string[],
|
||||||
|
acceleratedTxs: string[],
|
||||||
matchRate: number,
|
matchRate: number,
|
||||||
expectedFees?: number,
|
expectedFees?: number,
|
||||||
expectedWeight?: number,
|
expectedWeight?: number,
|
||||||
@ -91,6 +93,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
|
|||||||
block: number,
|
block: number,
|
||||||
vsize: number,
|
vsize: number,
|
||||||
};
|
};
|
||||||
|
acceleration?: boolean;
|
||||||
uid?: number;
|
uid?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,6 +185,7 @@ export interface TransactionStripped {
|
|||||||
fee: number;
|
fee: number;
|
||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
|
acc?: boolean;
|
||||||
rate?: number; // effective fee rate
|
rate?: number; // effective fee rate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,6 +116,7 @@ class AuditReplication {
|
|||||||
freshTxs: auditSummary.freshTxs || [],
|
freshTxs: auditSummary.freshTxs || [],
|
||||||
sigopTxs: auditSummary.sigopTxs || [],
|
sigopTxs: auditSummary.sigopTxs || [],
|
||||||
fullrbfTxs: auditSummary.fullrbfTxs || [],
|
fullrbfTxs: auditSummary.fullrbfTxs || [],
|
||||||
|
acceleratedTxs: auditSummary.acceleratedTxs || [],
|
||||||
matchRate: auditSummary.matchRate,
|
matchRate: auditSummary.matchRate,
|
||||||
expectedFees: auditSummary.expectedFees,
|
expectedFees: auditSummary.expectedFees,
|
||||||
expectedWeight: auditSummary.expectedWeight,
|
expectedWeight: auditSummary.expectedWeight,
|
||||||
|
@ -6,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
|
|||||||
class BlocksAuditRepositories {
|
class BlocksAuditRepositories {
|
||||||
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
public async $saveAudit(audit: BlockAudit): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, fullrbf_txs, match_rate, expected_fees, expected_weight)
|
await DB.query(`INSERT INTO blocks_audits(time, height, hash, missing_txs, added_txs, fresh_txs, sigop_txs, fullrbf_txs, accelerated_txs, match_rate, expected_fees, expected_weight)
|
||||||
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
VALUE (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [audit.time, audit.height, audit.hash, JSON.stringify(audit.missingTxs),
|
||||||
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
|
JSON.stringify(audit.addedTxs), JSON.stringify(audit.freshTxs), JSON.stringify(audit.sigopTxs), JSON.stringify(audit.fullrbfTxs), JSON.stringify(audit.acceleratedTxs), audit.matchRate, audit.expectedFees, audit.expectedWeight]);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
if (e.errno === 1062) { // ER_DUP_ENTRY - This scenario is possible upon node backend restart
|
||||||
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
|
logger.debug(`Cannot save block audit for block ${audit.hash} because it has already been indexed, ignoring`);
|
||||||
@ -69,6 +69,7 @@ class BlocksAuditRepositories {
|
|||||||
fresh_txs as freshTxs,
|
fresh_txs as freshTxs,
|
||||||
sigop_txs as sigopTxs,
|
sigop_txs as sigopTxs,
|
||||||
fullrbf_txs as fullrbfTxs,
|
fullrbf_txs as fullrbfTxs,
|
||||||
|
accelerated_txs as acceleratedTxs,
|
||||||
match_rate as matchRate,
|
match_rate as matchRate,
|
||||||
expected_fees as expectedFees,
|
expected_fees as expectedFees,
|
||||||
expected_weight as expectedWeight
|
expected_weight as expectedWeight
|
||||||
@ -83,6 +84,7 @@ class BlocksAuditRepositories {
|
|||||||
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
|
rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
|
||||||
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
|
rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
|
||||||
rows[0].fullrbfTxs = JSON.parse(rows[0].fullrbfTxs);
|
rows[0].fullrbfTxs = JSON.parse(rows[0].fullrbfTxs);
|
||||||
|
rows[0].acceleratedTxs = JSON.parse(rows[0].acceleratedTxs);
|
||||||
rows[0].template = JSON.parse(rows[0].template);
|
rows[0].template = JSON.parse(rows[0].template);
|
||||||
|
|
||||||
return rows[0];
|
return rows[0];
|
||||||
|
@ -40,7 +40,8 @@ class PoolsRepository {
|
|||||||
pools.link AS link,
|
pools.link AS link,
|
||||||
slug,
|
slug,
|
||||||
AVG(blocks_audits.match_rate) AS avgMatchRate,
|
AVG(blocks_audits.match_rate) AS avgMatchRate,
|
||||||
AVG((CAST(blocks.fees as SIGNED) - CAST(blocks_audits.expected_fees as SIGNED)) / NULLIF(CAST(blocks_audits.expected_fees as SIGNED), 0)) AS avgFeeDelta
|
AVG((CAST(blocks.fees as SIGNED) - CAST(blocks_audits.expected_fees as SIGNED)) / NULLIF(CAST(blocks_audits.expected_fees as SIGNED), 0)) AS avgFeeDelta,
|
||||||
|
unique_id as poolUniqueId
|
||||||
FROM blocks
|
FROM blocks
|
||||||
JOIN pools on pools.id = pool_id
|
JOIN pools on pools.id = pool_id
|
||||||
LEFT JOIN blocks_audits ON blocks_audits.height = blocks.height
|
LEFT JOIN blocks_audits ON blocks_audits.height = blocks.height
|
||||||
|
@ -135,6 +135,10 @@
|
|||||||
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
|
"AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
|
||||||
"SERVERS": __REPLICATION_SERVERS__
|
"SERVERS": __REPLICATION_SERVERS__
|
||||||
},
|
},
|
||||||
|
"MEMPOOL_SERVICES": {
|
||||||
|
"API": "__MEMPOOL_SERVICES_API__",
|
||||||
|
"ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
|
||||||
|
},
|
||||||
"REDIS": {
|
"REDIS": {
|
||||||
"ENABLED": __REDIS_ENABLED__,
|
"ENABLED": __REDIS_ENABLED__,
|
||||||
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"
|
"UNIX_SOCKET_PATH": "__REDIS_UNIX_SOCKET_PATH__"
|
||||||
|
@ -137,6 +137,10 @@ __REPLICATION_AUDIT__=${REPLICATION_AUDIT:=true}
|
|||||||
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
|
__REPLICATION_AUDIT_START_HEIGHT__=${REPLICATION_AUDIT_START_HEIGHT:=774000}
|
||||||
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
__REPLICATION_SERVERS__=${REPLICATION_SERVERS:=[]}
|
||||||
|
|
||||||
|
# MEMPOOL_SERVICES
|
||||||
|
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
|
||||||
|
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
|
||||||
|
|
||||||
# REDIS
|
# REDIS
|
||||||
__REDIS_ENABLED__=${REDIS_ENABLED:=true}
|
__REDIS_ENABLED__=${REDIS_ENABLED:=true}
|
||||||
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
|
__REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
|
||||||
@ -267,6 +271,10 @@ sed -i "s!__REPLICATION_AUDIT__!${__REPLICATION_AUDIT__}!g" mempool-config.json
|
|||||||
sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
|
sed -i "s!__REPLICATION_AUDIT_START_HEIGHT__!${__REPLICATION_AUDIT_START_HEIGHT__}!g" mempool-config.json
|
||||||
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json
|
sed -i "s!__REPLICATION_SERVERS__!${__REPLICATION_SERVERS__}!g" mempool-config.json
|
||||||
|
|
||||||
|
# MEMPOOL_SERVICES
|
||||||
|
sed -i "s!__MEMPOOL_SERVICES_API__!${__MEMPOOL_SERVICES_API__}!g" mempool-config.json
|
||||||
|
sed -i "s!__MEMPOOL_SERVICES_ACCELERATIONS__!${__MEMPOOL_SERVICES_ACCELERATIONS__}!g" mempool-config.json
|
||||||
|
|
||||||
# REDIS
|
# REDIS
|
||||||
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
|
sed -i "s!__REDIS_ENABLED__!${__REDIS_ENABLED__}!g" mempool-config.json
|
||||||
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
sed -i "s!__REDIS_UNIX_SOCKET_PATH__!${__REDIS_UNIX_SOCKET_PATH__}!g" mempool-config.json
|
||||||
|
@ -22,5 +22,6 @@
|
|||||||
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
||||||
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
||||||
"LIGHTNING": false,
|
"LIGHTNING": false,
|
||||||
"HISTORICAL_PRICE": true
|
"HISTORICAL_PRICE": true,
|
||||||
|
"ACCELERATOR": false
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,14 @@
|
|||||||
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
|
<track label="Português" kind="captions" srclang="pt" src="/resources/promo-video/pt.vtt" [attr.default]="showSubtitles('pt') ? '' : null">
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
|
<ng-container *ngIf="false && officialMempoolSpace">
|
||||||
|
<h3 class="mt-5">Sponsor the project</h3>
|
||||||
|
<div class="d-flex justify-content-center" style="max-width: 90%; margin: 35px auto 75px auto; column-gap: 15px">
|
||||||
|
<a href="/sponsor" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.community-sponsor-button">Community</a>
|
||||||
|
<a href="/enterprise" class="btn" style="background-color: rgba(152, 88, 255, 0.75); box-shadow: 0px 0px 50px 5px rgba(152, 88, 255, 0.75)" i18n="about.enterprise-sponsor-button">Enterprise</a>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<div class="enterprise-sponsor" id="enterprise-sponsors">
|
<div class="enterprise-sponsor" id="enterprise-sponsors">
|
||||||
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
|
<h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
@ -191,16 +199,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="community-sponsor" id="community-sponsors">
|
<ng-container *ngIf="officialMempoolSpace">
|
||||||
<h3 i18n="about.sponsors.withHeart">Community Sponsors ❤️</h3>
|
<div *ngIf="profiles$ | async as profiles" id="community-sponsors">
|
||||||
|
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.whales.length > 0">
|
||||||
|
<h3 i18n="about.sponsors.withHeart">Whale Sponsors</h3>
|
||||||
|
<div class="wrapper">
|
||||||
|
<ng-container>
|
||||||
|
<ng-template ngFor let-sponsor [ngForOf]="profiles.whales">
|
||||||
|
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
|
||||||
|
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||||
|
</a>
|
||||||
|
</ng-template>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="community-sponsor" style="margin-bottom: 68px" *ngIf="profiles.chads.length > 0">
|
||||||
|
<h3 i18n="about.sponsors.withHeart">Chad Sponsors</h3>
|
||||||
|
<div class="wrapper">
|
||||||
|
<ng-template ngFor let-sponsor [ngForOf]="profiles.chads">
|
||||||
|
<a [href]="'https://twitter.com/' + sponsor.username" target="_blank" rel="sponsored" [title]="sponsor.username">
|
||||||
|
<img class="image" [src]="'data:' + sponsor.image_mime + ';base64,' + sponsor.image" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||||
|
</a>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<div class="community-sponsor" style="margin-bottom: 68px">
|
||||||
|
<h3 i18n="about.sponsors.withHeart">OG Sponsors ❤️</h3>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<ng-container *ngIf="sponsors$ | async as sponsors; else loadingSponsors">
|
<ng-container *ngIf="ogs$ | async as ogs; else loadingSponsors">
|
||||||
<ng-template ngFor let-sponsor [ngForOf]="sponsors">
|
<a *ngFor="let ogSponsor of ogs" [href]="'https://twitter.com/' + ogSponsor.handle" target="_blank" rel="sponsored" [title]="ogSponsor.handle">
|
||||||
<a [href]="'https://twitter.com/' + sponsor.handle" target="_blank" rel="sponsored" [title]="sponsor.handle">
|
<img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||||
<img class="image" [src]="'/api/v1/donations/images/' + sponsor.handle" />
|
</a>
|
||||||
</a>
|
|
||||||
</ng-template>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -340,7 +373,7 @@
|
|||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<ng-template ngFor let-translator [ngForOf]="translators">
|
<ng-template ngFor let-translator [ngForOf]="translators">
|
||||||
<a [href]="'https://twitter.com/' + translator.value" target="_blank" [title]="translator.key">
|
<a [href]="'https://twitter.com/' + translator.value" target="_blank" [title]="translator.key">
|
||||||
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" />
|
<img class="image" [src]="'/api/v1/translators/images/' + translator.value" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||||
</a>
|
</a>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
@ -354,7 +387,7 @@
|
|||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<ng-template ngFor let-contributor [ngForOf]="contributors.regular">
|
<ng-template ngFor let-contributor [ngForOf]="contributors.regular">
|
||||||
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
|
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
|
||||||
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" />
|
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||||
<span>{{ contributor.name }}</span>
|
<span>{{ contributor.name }}</span>
|
||||||
</a>
|
</a>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@ -366,7 +399,7 @@
|
|||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<ng-template ngFor let-contributor [ngForOf]="contributors.core">
|
<ng-template ngFor let-contributor [ngForOf]="contributors.core">
|
||||||
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
|
<a [href]="'https://github.com/' + contributor.name" target="_blank" [title]="contributor.name">
|
||||||
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" />
|
<img class="image" [src]="'/api/v1/contributors/images/' + contributor.id" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
|
||||||
<span>{{ contributor.name }}</span>
|
<span>{{ contributor.name }}</span>
|
||||||
</a>
|
</a>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -10,6 +10,9 @@
|
|||||||
margin: 25px;
|
margin: 25px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
}
|
}
|
||||||
|
.unknown {
|
||||||
|
border: 1px solid #b4b4b4;
|
||||||
|
}
|
||||||
|
|
||||||
.image.not-rounded {
|
.image.not-rounded {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
@ -6,7 +6,7 @@ import { Observable } from 'rxjs';
|
|||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { IBackendInfo } from '../../interfaces/websocket.interface';
|
import { IBackendInfo } from '../../interfaces/websocket.interface';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
import { map, tap } from 'rxjs/operators';
|
import { map, share, tap } from 'rxjs/operators';
|
||||||
import { ITranslators } from '../../interfaces/node-api.interface';
|
import { ITranslators } from '../../interfaces/node-api.interface';
|
||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
|
|
||||||
@ -19,14 +19,16 @@ import { DOCUMENT } from '@angular/common';
|
|||||||
export class AboutComponent implements OnInit {
|
export class AboutComponent implements OnInit {
|
||||||
@ViewChild('promoVideo') promoVideo: ElementRef;
|
@ViewChild('promoVideo') promoVideo: ElementRef;
|
||||||
backendInfo$: Observable<IBackendInfo>;
|
backendInfo$: Observable<IBackendInfo>;
|
||||||
sponsors$: Observable<any>;
|
|
||||||
translators$: Observable<ITranslators>;
|
|
||||||
allContributors$: Observable<any>;
|
|
||||||
frontendGitCommitHash = this.stateService.env.GIT_COMMIT_HASH;
|
frontendGitCommitHash = this.stateService.env.GIT_COMMIT_HASH;
|
||||||
packetJsonVersion = this.stateService.env.PACKAGE_JSON_VERSION;
|
packetJsonVersion = this.stateService.env.PACKAGE_JSON_VERSION;
|
||||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||||
showNavigateToSponsor = false;
|
showNavigateToSponsor = false;
|
||||||
|
|
||||||
|
profiles$: Observable<any>;
|
||||||
|
translators$: Observable<ITranslators>;
|
||||||
|
allContributors$: Observable<any>;
|
||||||
|
ogs$: Observable<any>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
@ -43,10 +45,13 @@ export class AboutComponent implements OnInit {
|
|||||||
this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`);
|
this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`);
|
||||||
this.websocketService.want(['blocks']);
|
this.websocketService.want(['blocks']);
|
||||||
|
|
||||||
this.sponsors$ = this.apiService.getDonation$()
|
this.profiles$ = this.apiService.getAboutPageProfiles$().pipe(
|
||||||
.pipe(
|
tap(() => {
|
||||||
tap(() => this.goToAnchor())
|
this.goToAnchor()
|
||||||
);
|
}),
|
||||||
|
share(),
|
||||||
|
)
|
||||||
|
|
||||||
this.translators$ = this.apiService.getTranslators$()
|
this.translators$ = this.apiService.getTranslators$()
|
||||||
.pipe(
|
.pipe(
|
||||||
map((translators) => {
|
map((translators) => {
|
||||||
@ -59,6 +64,9 @@ export class AboutComponent implements OnInit {
|
|||||||
}),
|
}),
|
||||||
tap(() => this.goToAnchor())
|
tap(() => this.goToAnchor())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.ogs$ = this.apiService.getOgs$();
|
||||||
|
|
||||||
this.allContributors$ = this.apiService.getContributor$().pipe(
|
this.allContributors$ = this.apiService.getContributor$().pipe(
|
||||||
map((contributors) => {
|
map((contributors) => {
|
||||||
return {
|
return {
|
||||||
|
@ -147,7 +147,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||||
if (this.scene) {
|
if (this.scene) {
|
||||||
this.scene.update(add, remove, change, direction, resetLayout);
|
this.scene.update(add, remove, change, direction, resetLayout);
|
||||||
this.start();
|
this.start();
|
||||||
|
@ -150,7 +150,7 @@ export default class BlockScene {
|
|||||||
this.updateAll(startTime, 200, direction);
|
this.updateAll(startTime, 200, direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
const removed = this.removeBatch(remove, startTime, direction);
|
const removed = this.removeBatch(remove, startTime, direction);
|
||||||
|
|
||||||
@ -175,6 +175,7 @@ export default class BlockScene {
|
|||||||
// update effective rates
|
// update effective rates
|
||||||
change.forEach(tx => {
|
change.forEach(tx => {
|
||||||
if (this.txs[tx.txid]) {
|
if (this.txs[tx.txid]) {
|
||||||
|
this.txs[tx.txid].acc = tx.acc;
|
||||||
this.txs[tx.txid].feerate = tx.rate || (this.txs[tx.txid].fee / this.txs[tx.txid].vsize);
|
this.txs[tx.txid].feerate = tx.rate || (this.txs[tx.txid].fee / this.txs[tx.txid].vsize);
|
||||||
this.txs[tx.txid].rate = tx.rate;
|
this.txs[tx.txid].rate = tx.rate;
|
||||||
this.txs[tx.txid].dirty = true;
|
this.txs[tx.txid].dirty = true;
|
||||||
|
@ -17,6 +17,7 @@ const auditColors = {
|
|||||||
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
|
||||||
added: hexToColor('0099ff'),
|
added: hexToColor('0099ff'),
|
||||||
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
|
||||||
|
accelerated: hexToColor('8F5FF6'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// convert from this class's update format to TxSprite's update format
|
// convert from this class's update format to TxSprite's update format
|
||||||
@ -37,8 +38,9 @@ export default class TxView implements TransactionStripped {
|
|||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
feerate: number;
|
feerate: number;
|
||||||
|
acc?: boolean;
|
||||||
rate?: number;
|
rate?: number;
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
|
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||||
context?: 'projected' | 'actual';
|
context?: 'projected' | 'actual';
|
||||||
scene?: BlockScene;
|
scene?: BlockScene;
|
||||||
|
|
||||||
@ -63,6 +65,7 @@ export default class TxView implements TransactionStripped {
|
|||||||
this.vsize = tx.vsize;
|
this.vsize = tx.vsize;
|
||||||
this.value = tx.value;
|
this.value = tx.value;
|
||||||
this.feerate = tx.rate || (tx.fee / tx.vsize); // sort by effective fee rate where available
|
this.feerate = tx.rate || (tx.fee / tx.vsize); // sort by effective fee rate where available
|
||||||
|
this.acc = tx.acc;
|
||||||
this.rate = tx.rate;
|
this.rate = tx.rate;
|
||||||
this.status = tx.status;
|
this.status = tx.status;
|
||||||
this.initialised = false;
|
this.initialised = false;
|
||||||
@ -199,6 +202,11 @@ export default class TxView implements TransactionStripped {
|
|||||||
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
|
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
|
||||||
// Normal mode
|
// Normal mode
|
||||||
if (!this.scene?.highlightingEnabled) {
|
if (!this.scene?.highlightingEnabled) {
|
||||||
|
if (this.acc) {
|
||||||
|
return auditColors.accelerated;
|
||||||
|
} else {
|
||||||
|
return feeLevelColor;
|
||||||
|
}
|
||||||
return feeLevelColor;
|
return feeLevelColor;
|
||||||
}
|
}
|
||||||
// Block audit
|
// Block audit
|
||||||
@ -216,6 +224,8 @@ export default class TxView implements TransactionStripped {
|
|||||||
return auditColors.added;
|
return auditColors.added;
|
||||||
case 'selected':
|
case 'selected':
|
||||||
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
|
||||||
|
case 'accelerated':
|
||||||
|
return auditColors.accelerated;
|
||||||
case 'found':
|
case 'found':
|
||||||
if (this.context === 'projected') {
|
if (this.context === 'projected') {
|
||||||
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
|
||||||
@ -223,7 +233,11 @@ export default class TxView implements TransactionStripped {
|
|||||||
return feeLevelColor;
|
return feeLevelColor;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return feeLevelColor;
|
if (this.acc) {
|
||||||
|
return auditColors.accelerated;
|
||||||
|
} else {
|
||||||
|
return feeLevelColor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,8 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="effectiveRate && effectiveRate !== feeRate">
|
<tr *ngIf="effectiveRate && effectiveRate !== feeRate">
|
||||||
<td class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
<td *ngIf="!this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||||
|
<td *ngIf="this.acceleration" class="td-width" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Accelerated fee rate</td>
|
||||||
<td>
|
<td>
|
||||||
<app-fee-rate [fee]="effectiveRate"></app-fee-rate>
|
<app-fee-rate [fee]="effectiveRate"></app-fee-rate>
|
||||||
</td>
|
</td>
|
||||||
@ -54,6 +55,7 @@
|
|||||||
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
<td *ngSwitchCase="'added'"><span class="badge badge-warning" i18n="transaction.audit.added">Added</span></td>
|
||||||
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
<td *ngSwitchCase="'selected'"><span class="badge badge-warning" i18n="transaction.audit.marginal">Marginal fee rate</span></td>
|
||||||
<td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td>
|
<td *ngSwitchCase="'rbf'"><span class="badge badge-warning" i18n="transaction.audit.conflicting">Conflicting</span></td>
|
||||||
|
<td *ngSwitchCase="'accelerated'"><span class="badge badge-success" i18n="transaction.audit.accelerated">Accelerated</span></td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -21,6 +21,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
|
|||||||
vsize = 1;
|
vsize = 1;
|
||||||
feeRate = 0;
|
feeRate = 0;
|
||||||
effectiveRate;
|
effectiveRate;
|
||||||
|
acceleration;
|
||||||
|
|
||||||
tooltipPosition: Position = { x: 0, y: 0 };
|
tooltipPosition: Position = { x: 0, y: 0 };
|
||||||
|
|
||||||
@ -53,6 +54,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
|
|||||||
this.vsize = tx.vsize || 1;
|
this.vsize = tx.vsize || 1;
|
||||||
this.feeRate = this.fee / this.vsize;
|
this.feeRate = this.fee / this.vsize;
|
||||||
this.effectiveRate = tx.rate;
|
this.effectiveRate = tx.rate;
|
||||||
|
this.acceleration = tx.acc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -340,12 +340,16 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
const isFresh = {};
|
const isFresh = {};
|
||||||
const isSigop = {};
|
const isSigop = {};
|
||||||
const isRbf = {};
|
const isRbf = {};
|
||||||
|
const isAccelerated = {};
|
||||||
this.numMissing = 0;
|
this.numMissing = 0;
|
||||||
this.numUnexpected = 0;
|
this.numUnexpected = 0;
|
||||||
|
|
||||||
if (blockAudit?.template) {
|
if (blockAudit?.template) {
|
||||||
for (const tx of blockAudit.template) {
|
for (const tx of blockAudit.template) {
|
||||||
inTemplate[tx.txid] = true;
|
inTemplate[tx.txid] = true;
|
||||||
|
if (tx.acc) {
|
||||||
|
isAccelerated[tx.txid] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const tx of transactions) {
|
for (const tx of transactions) {
|
||||||
inBlock[tx.txid] = true;
|
inBlock[tx.txid] = true;
|
||||||
@ -365,6 +369,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
for (const txid of blockAudit.fullrbfTxs || []) {
|
for (const txid of blockAudit.fullrbfTxs || []) {
|
||||||
isRbf[txid] = true;
|
isRbf[txid] = true;
|
||||||
}
|
}
|
||||||
|
for (const txid of blockAudit.acceleratedTxs || []) {
|
||||||
|
isAccelerated[txid] = true;
|
||||||
|
}
|
||||||
// set transaction statuses
|
// set transaction statuses
|
||||||
for (const tx of blockAudit.template) {
|
for (const tx of blockAudit.template) {
|
||||||
tx.context = 'projected';
|
tx.context = 'projected';
|
||||||
@ -389,6 +396,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
isMissing[tx.txid] = true;
|
isMissing[tx.txid] = true;
|
||||||
this.numMissing++;
|
this.numMissing++;
|
||||||
}
|
}
|
||||||
|
if (isAccelerated[tx.txid]) {
|
||||||
|
tx.status = 'accelerated';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const [index, tx] of transactions.entries()) {
|
for (const [index, tx] of transactions.entries()) {
|
||||||
tx.context = 'actual';
|
tx.context = 'actual';
|
||||||
@ -405,6 +415,9 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||||||
isSelected[tx.txid] = true;
|
isSelected[tx.txid] = true;
|
||||||
this.numUnexpected++;
|
this.numUnexpected++;
|
||||||
}
|
}
|
||||||
|
if (isAccelerated[tx.txid]) {
|
||||||
|
tx.status = 'accelerated';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const tx of transactions) {
|
for (const tx of transactions) {
|
||||||
inBlock[tx.txid] = true;
|
inBlock[tx.txid] = true;
|
||||||
|
@ -64,7 +64,7 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const samples = [];
|
const samples = [];
|
||||||
const txs = this.transactions.map(tx => { return { vsize: tx.vsize, rate: tx.rate || (tx.fee / tx.vsize) }; }).sort((a, b) => { return b.rate - a.rate; });
|
const txs = this.transactions.filter(tx => !tx.acc).map(tx => { return { vsize: tx.vsize, rate: tx.rate || (tx.fee / tx.vsize) }; }).sort((a, b) => { return b.rate - a.rate; });
|
||||||
const maxBlockVSize = this.stateService.env.BLOCK_WEIGHT_UNITS / 4;
|
const maxBlockVSize = this.stateService.env.BLOCK_WEIGHT_UNITS / 4;
|
||||||
const sampleInterval = maxBlockVSize / this.numSamples;
|
const sampleInterval = maxBlockVSize / this.numSamples;
|
||||||
let cumVSize = 0;
|
let cumVSize = 0;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<ng-container *ngIf="{ val: network$ | async } as network">
|
<ng-container *ngIf="{ val: network$ | async } as network">
|
||||||
<header>
|
<header *ngIf="headerVisible">
|
||||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||||
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
|
<a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
|
||||||
<ng-template [ngIf]="subdomain">
|
<ng-template [ngIf]="subdomain">
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit, Input } from '@angular/core';
|
||||||
import { Env, StateService } from '../../services/state.service';
|
import { Env, StateService } from '../../services/state.service';
|
||||||
import { Observable, merge, of } from 'rxjs';
|
import { Observable, merge, of } from 'rxjs';
|
||||||
import { LanguageService } from '../../services/language.service';
|
import { LanguageService } from '../../services/language.service';
|
||||||
@ -11,6 +11,9 @@ import { NavigationService } from '../../services/navigation.service';
|
|||||||
styleUrls: ['./master-page.component.scss'],
|
styleUrls: ['./master-page.component.scss'],
|
||||||
})
|
})
|
||||||
export class MasterPageComponent implements OnInit {
|
export class MasterPageComponent implements OnInit {
|
||||||
|
@Input() headerVisible = true;
|
||||||
|
@Input() footerVisibleOverride: boolean | null = null;
|
||||||
|
|
||||||
env: Env;
|
env: Env;
|
||||||
network$: Observable<string>;
|
network$: Observable<string>;
|
||||||
connectionState$: Observable<number>;
|
connectionState$: Observable<number>;
|
||||||
@ -38,10 +41,14 @@ export class MasterPageComponent implements OnInit {
|
|||||||
this.subdomain = this.enterpriseService.getSubdomain();
|
this.subdomain = this.enterpriseService.getSubdomain();
|
||||||
this.navigationService.subnetPaths.subscribe((paths) => {
|
this.navigationService.subnetPaths.subscribe((paths) => {
|
||||||
this.networkPaths = paths;
|
this.networkPaths = paths;
|
||||||
if (paths.mainnet.indexOf('docs') > -1) {
|
if (this.footerVisibleOverride === null) {
|
||||||
this.footerVisible = false;
|
if (paths.mainnet.indexOf('docs') > -1) {
|
||||||
|
this.footerVisible = false;
|
||||||
|
} else {
|
||||||
|
this.footerVisible = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.footerVisible = true;
|
this.footerVisible = this.footerVisibleOverride;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
|
|||||||
|
|
||||||
updateBlock(delta: MempoolBlockDelta): void {
|
updateBlock(delta: MempoolBlockDelta): void {
|
||||||
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
|
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
|
||||||
|
|
||||||
if (this.blockIndex !== this.index) {
|
if (this.blockIndex !== this.index) {
|
||||||
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection;
|
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection;
|
||||||
this.blockGraph.replace(delta.added, direction);
|
this.blockGraph.replace(delta.added, direction);
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="arrowVisible" id="arrow-up" [ngStyle]="{'right': rightPosition + 75 + 'px', transition: transition }"></div>
|
<div *ngIf="arrowVisible" id="arrow-up" [ngStyle]="{'right': rightPosition + 75 + 'px', transition: transition }" [class.blink]="txPosition?.accelerated"></div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -169,4 +169,34 @@
|
|||||||
transform: translate(calc(-0.2 * var(--block-size)), calc(1.1 * var(--block-size)));
|
transform: translate(calc(-0.2 * var(--block-size)), calc(1.1 * var(--block-size)));
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blink{
|
||||||
|
width:400px;
|
||||||
|
height:400px;
|
||||||
|
border-bottom: 35px solid #FFF;
|
||||||
|
animation: blink 0.2s infinite;
|
||||||
|
}
|
||||||
|
@keyframes blink{
|
||||||
|
0% {
|
||||||
|
border-bottom: 35px solid green;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
border-bottom: 35px solid yellow;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
border-bottom: 35px solid orange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes blink{
|
||||||
|
0% {
|
||||||
|
border-bottom: 35px solid green;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
border-bottom: 35px solid yellow;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
border-bottom: 35px solid orange;
|
||||||
|
}
|
||||||
}
|
}
|
@ -26,6 +26,7 @@ import { animate, style, transition, trigger } from '@angular/animations';
|
|||||||
export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
@Input() minimal: boolean = false;
|
@Input() minimal: boolean = false;
|
||||||
@Input() blockWidth: number = 125;
|
@Input() blockWidth: number = 125;
|
||||||
|
@Input() containerWidth: number = null;
|
||||||
@Input() count: number = null;
|
@Input() count: number = null;
|
||||||
@Input() spotlight: number = 0;
|
@Input() spotlight: number = 0;
|
||||||
@Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`;
|
@Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`;
|
||||||
|
@ -99,14 +99,20 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #estimationTmpl>
|
<ng-template #estimationTmpl>
|
||||||
<ng-template [ngIf]="this.mempoolPosition.block >= 7" [ngIfElse]="belowBlockLimit">
|
<ng-template [ngIf]="this.mempoolPosition.block >= 7" [ngIfElse]="belowBlockLimit">
|
||||||
<span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span>
|
<span class="eta d-flex">
|
||||||
|
<span i18n="transaction.eta.in-several-hours|Transaction ETA in several hours or more">In several hours (or more)</span>
|
||||||
|
<span class="ml-2"></span><a *ngIf="stateService.env.OFFICIAL_MEMPOOL_SPACE && stateService.env.ACCELERATOR" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn badge badge-primary accelerate ml-auto" i18n="transaction.accelerate|Accelerate button label">Accelerate</a>
|
||||||
|
</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #belowBlockLimit>
|
<ng-template #belowBlockLimit>
|
||||||
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeEstimateDefault">
|
<ng-template [ngIf]="network === 'liquid' || network === 'liquidtestnet'" [ngIfElse]="timeEstimateDefault">
|
||||||
<app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time>
|
<app-time kind="until" [time]="(60 * 1000 * this.mempoolPosition.block) + now" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #timeEstimateDefault>
|
<ng-template #timeEstimateDefault>
|
||||||
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.timeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
|
<span class="d-flex">
|
||||||
|
<app-time kind="until" *ngIf="(da$ | async) as da;" [time]="da.timeAvg * (this.mempoolPosition.block + 1) + now + da.timeOffset" [fastRender]="false" [fixedRender]="true"></app-time>
|
||||||
|
<span class="ml-2"></span><a *ngIf="stateService.env.OFFICIAL_MEMPOOL_SPACE && stateService.env.ACCELERATOR" [href]="'/services/accelerator/accelerate?txid=' + tx.txid" class="btn badge badge-primary accelerate ml-auto" i18n="transaction.accelerate|Accelerate button label">Accelerate</a>
|
||||||
|
</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@ -488,7 +494,8 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="cpfpInfo && hasEffectiveFeeRate">
|
<tr *ngIf="cpfpInfo && hasEffectiveFeeRate">
|
||||||
<td i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
<td *ngIf="tx.acceleration" i18n="transaction.accelerated-fee-rate|Accelerated transaction fee rate">Accelerated fee rate</td>
|
||||||
|
<td *ngIf="!tx.acceleration" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="effective-fee-container">
|
<div class="effective-fee-container">
|
||||||
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||||
|
@ -216,4 +216,23 @@
|
|||||||
.alert-link {
|
.alert-link {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.eta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-content: center;
|
||||||
|
@media (min-width: 850px) {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accelerate {
|
||||||
|
align-self: auto;
|
||||||
|
margin-top: 3px;
|
||||||
|
@media (min-width: 850px) {
|
||||||
|
justify-self: start;
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
}
|
}
|
@ -97,7 +97,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private relativeUrlPipe: RelativeUrlPipe,
|
private relativeUrlPipe: RelativeUrlPipe,
|
||||||
private electrsApiService: ElectrsApiService,
|
private electrsApiService: ElectrsApiService,
|
||||||
private stateService: StateService,
|
public stateService: StateService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private audioService: AudioService,
|
private audioService: AudioService,
|
||||||
@ -183,6 +183,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
|
this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
|
||||||
}
|
}
|
||||||
|
if (cpfpInfo.acceleration) {
|
||||||
|
this.tx.acceleration = cpfpInfo.acceleration;
|
||||||
|
}
|
||||||
|
|
||||||
this.cpfpInfo = cpfpInfo;
|
this.cpfpInfo = cpfpInfo;
|
||||||
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
|
this.hasEffectiveFeeRate = hasRelatives || (this.tx.effectiveFeePerVsize && (Math.abs(this.tx.effectiveFeePerVsize - this.tx.feePerVsize) > 0.01));
|
||||||
|
@ -19,6 +19,7 @@ export interface Transaction {
|
|||||||
ancestors?: Ancestor[];
|
ancestors?: Ancestor[];
|
||||||
bestDescendant?: BestDescendant | null;
|
bestDescendant?: BestDescendant | null;
|
||||||
cpfpChecked?: boolean;
|
cpfpChecked?: boolean;
|
||||||
|
acceleration?: number;
|
||||||
deleteAfter?: number;
|
deleteAfter?: number;
|
||||||
_unblinded?: any;
|
_unblinded?: any;
|
||||||
_deduced?: boolean;
|
_deduced?: boolean;
|
||||||
|
@ -27,6 +27,7 @@ export interface CpfpInfo {
|
|||||||
effectiveFeePerVsize?: number;
|
effectiveFeePerVsize?: number;
|
||||||
sigops?: number;
|
sigops?: number;
|
||||||
adjustedVsize?: number;
|
adjustedVsize?: number;
|
||||||
|
acceleration?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RbfInfo {
|
export interface RbfInfo {
|
||||||
@ -111,6 +112,7 @@ export interface PoolInfo {
|
|||||||
addresses: string; // JSON array
|
addresses: string; // JSON array
|
||||||
emptyBlocks: number;
|
emptyBlocks: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
poolUniqueId: number;
|
||||||
}
|
}
|
||||||
export interface PoolStat {
|
export interface PoolStat {
|
||||||
pool: PoolInfo;
|
pool: PoolInfo;
|
||||||
@ -159,6 +161,7 @@ export interface BlockAudit extends BlockExtended {
|
|||||||
freshTxs: string[],
|
freshTxs: string[],
|
||||||
sigopTxs: string[],
|
sigopTxs: string[],
|
||||||
fullrbfTxs: string[],
|
fullrbfTxs: string[],
|
||||||
|
acceleratedTxs: string[],
|
||||||
matchRate: number,
|
matchRate: number,
|
||||||
expectedFees: number,
|
expectedFees: number,
|
||||||
expectedWeight: number,
|
expectedWeight: number,
|
||||||
@ -175,7 +178,8 @@ export interface TransactionStripped {
|
|||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
rate?: number; // effective fee rate
|
rate?: number; // effective fee rate
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
|
acc?: boolean;
|
||||||
|
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||||
context?: 'projected' | 'actual';
|
context?: 'projected' | 'actual';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,6 +191,7 @@ export interface RbfTransaction extends TransactionStripped {
|
|||||||
export interface MempoolPosition {
|
export interface MempoolPosition {
|
||||||
block: number,
|
block: number,
|
||||||
vsize: number,
|
vsize: number,
|
||||||
|
accelerated?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RewardStats {
|
export interface RewardStats {
|
||||||
|
@ -70,7 +70,7 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
|
|||||||
export interface MempoolBlockDelta {
|
export interface MempoolBlockDelta {
|
||||||
added: TransactionStripped[],
|
added: TransactionStripped[],
|
||||||
removed: string[],
|
removed: string[],
|
||||||
changed?: { txid: string, rate: number | undefined }[];
|
changed?: { txid: string, rate: number | undefined, acc: boolean | undefined }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MempoolInfo {
|
export interface MempoolInfo {
|
||||||
@ -88,8 +88,9 @@ export interface TransactionStripped {
|
|||||||
fee: number;
|
fee: number;
|
||||||
vsize: number;
|
vsize: number;
|
||||||
value: number;
|
value: number;
|
||||||
|
acc?: boolean; // is accelerated?
|
||||||
rate?: number; // effective fee rate
|
rate?: number; // effective fee rate
|
||||||
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf';
|
status?: 'found' | 'missing' | 'sigop' | 'fresh' | 'freshcpfp' | 'added' | 'censored' | 'selected' | 'rbf' | 'accelerated';
|
||||||
context?: 'projected' | 'actual';
|
context?: 'projected' | 'actual';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ import { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-node
|
|||||||
import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component';
|
import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component';
|
||||||
import { NodeChannels } from '../lightning/nodes-channels/node-channels.component';
|
import { NodeChannels } from '../lightning/nodes-channels/node-channels.component';
|
||||||
import { GroupComponent } from './group/group.component';
|
import { GroupComponent } from './group/group.component';
|
||||||
|
import { NodeOwnerComponent } from './node-owner/node-owner.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -66,6 +67,7 @@ import { GroupComponent } from './group/group.component';
|
|||||||
NodesRankingsDashboard,
|
NodesRankingsDashboard,
|
||||||
NodeChannels,
|
NodeChannels,
|
||||||
GroupComponent,
|
GroupComponent,
|
||||||
|
NodeOwnerComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@ -103,6 +105,7 @@ import { GroupComponent } from './group/group.component';
|
|||||||
OldestNodes,
|
OldestNodes,
|
||||||
NodesRankingsDashboard,
|
NodesRankingsDashboard,
|
||||||
NodeChannels,
|
NodeChannels,
|
||||||
|
NodeOwnerComponent,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
LightningApiService,
|
LightningApiService,
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
<div *ngIf="stateService.env.OFFICIAL_MEMPOOL_SPACE === true">
|
||||||
|
|
||||||
|
<div *ngIf="{ value: (nodeOwner$ | async) } as nodeOwner">
|
||||||
|
|
||||||
|
<div *ngIf="nodeOwner.value && nodeOwner.value.sns_id">
|
||||||
|
<a target="_blank" [href]="'https://twitter.com/' + nodeOwner.value.username">
|
||||||
|
<img class="profile-photo" [src]="'data:' + nodeOwner.value.image_mime + ';base64,' + nodeOwner.value.image">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="nodeOwner.value === false">
|
||||||
|
<a [href]="'/login/lnnode?type=signup&pubkey=' + publicKey + '&alias=' + alias" class="btn btn-primary btn-sm">Claim</a>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
@ -0,0 +1,4 @@
|
|||||||
|
.profile-photo {
|
||||||
|
width: 31px;
|
||||||
|
height: 31px;
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { StateService } from '../../services/state.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-node-owner',
|
||||||
|
templateUrl: './node-owner.component.html',
|
||||||
|
styleUrls: ['./node-owner.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class NodeOwnerComponent{
|
||||||
|
@Input() publicKey: string = '';
|
||||||
|
@Input() alias: string = '';
|
||||||
|
@Input() nodeOwner$: Observable<any>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public stateService: StateService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
@ -3,13 +3,17 @@
|
|||||||
<ng-container *ngIf="!error">
|
<ng-container *ngIf="!error">
|
||||||
<h5 class="mb-0" style="color: #ffffff66" i18n="lightning.node">Lightning node</h5>
|
<h5 class="mb-0" style="color: #ffffff66" i18n="lightning.node">Lightning node</h5>
|
||||||
<div class="title-container mb-2">
|
<div class="title-container mb-2">
|
||||||
<h1 class="mb-0 text-truncate">{{ node.alias }}</h1>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<span class="tx-link">
|
<h1 class="mb-0 text-truncate">{{ node.alias }}</h1>
|
||||||
|
<!-- <app-node-owner [nodeOwner$]="nodeOwner$" [publicKey]="node.public_key" [alias]="node.alias" class="claim-btn"></app-node-owner> -->
|
||||||
|
</div>
|
||||||
|
<span class="tx-link justify-content-between align-items-center">
|
||||||
<span class="node-id">
|
<span class="node-id">
|
||||||
<app-truncate [text]="node.public_key" [lastChars]="8" [link]="['/lightning/node' | relativeUrl, node.public_key]">
|
<app-truncate [text]="node.public_key" [lastChars]="8" [link]="['/lightning/node' | relativeUrl, node.public_key]">
|
||||||
<app-clipboard [text]="node.public_key"></app-clipboard>
|
<app-clipboard [text]="node.public_key"></app-clipboard>
|
||||||
</app-truncate>
|
</app-truncate>
|
||||||
</span>
|
</span>
|
||||||
|
<!-- <app-node-owner [nodeOwner$]="nodeOwner$" [publicKey]="node.public_key" [alias]="node.alias" class="claim-btn-mobile"></app-node-owner> -->
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@ -111,3 +111,17 @@ app-fiat {
|
|||||||
margin: 0 0.25em;
|
margin: 0 0.25em;
|
||||||
color: slategrey;
|
color: slategrey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.claim-btn {
|
||||||
|
max-height: 32px;
|
||||||
|
@media (min-width: 850px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-btn-mobile {
|
||||||
|
max-height: 32px;
|
||||||
|
@media (max-width: 850px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit, ChangeDetectorRef } from '@angular/core';
|
||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of, EMPTY } from 'rxjs';
|
||||||
import { catchError, map, switchMap, tap } from 'rxjs/operators';
|
import { catchError, map, switchMap, tap, share } from 'rxjs/operators';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { LightningApiService } from '../lightning-api.service';
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
@ -38,6 +38,7 @@ export class NodeComponent implements OnInit {
|
|||||||
tlvRecords: CustomRecord[];
|
tlvRecords: CustomRecord[];
|
||||||
avgChannelDistance$: Observable<number | null>;
|
avgChannelDistance$: Observable<number | null>;
|
||||||
showFeatures = false;
|
showFeatures = false;
|
||||||
|
nodeOwner$: Observable<any>;
|
||||||
kmToMiles = kmToMiles;
|
kmToMiles = kmToMiles;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -45,6 +46,7 @@ export class NodeComponent implements OnInit {
|
|||||||
private lightningApiService: LightningApiService,
|
private lightningApiService: LightningApiService,
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private seoService: SeoService,
|
private seoService: SeoService,
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -147,6 +149,24 @@ export class NodeComponent implements OnInit {
|
|||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
) as Observable<number | null>;
|
) as Observable<number | null>;
|
||||||
|
|
||||||
|
this.nodeOwner$ = this.activatedRoute.paramMap
|
||||||
|
.pipe(
|
||||||
|
switchMap((params: ParamMap) => {
|
||||||
|
return this.apiService.getNodeOwner$(params.get('public_key')).pipe(
|
||||||
|
switchMap((response) => {
|
||||||
|
if (response.status === 204) {
|
||||||
|
return of(false);
|
||||||
|
}
|
||||||
|
return of(response.body);
|
||||||
|
}),
|
||||||
|
catchError(() => {
|
||||||
|
return of(false);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
share(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleShowDetails(): void {
|
toggleShowDetails(): void {
|
||||||
|
@ -8,6 +8,8 @@ import { WebsocketResponse } from '../interfaces/websocket.interface';
|
|||||||
import { Outspend, Transaction } from '../interfaces/electrs.interface';
|
import { Outspend, Transaction } from '../interfaces/electrs.interface';
|
||||||
import { Conversion } from './price.service';
|
import { Conversion } from './price.service';
|
||||||
|
|
||||||
|
const SERVICES_API_PREFIX = `/api/v1/services`;
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
@ -92,15 +94,11 @@ export class ApiService {
|
|||||||
return this.httpClient.get<Outspend[][]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/outspends', { params });
|
return this.httpClient.get<Outspend[][]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/outspends', { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
requestDonation$(amount: number, orderId: string): Observable<any> {
|
getAboutPageProfiles$(): Observable<any[]> {
|
||||||
const params = {
|
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/about-page');
|
||||||
amount: amount,
|
|
||||||
orderId: orderId,
|
|
||||||
};
|
|
||||||
return this.httpClient.post<any>(this.apiBaseUrl + '/api/v1/donations', params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getDonation$(): Observable<any[]> {
|
getOgs$(): Observable<any> {
|
||||||
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/donations');
|
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/donations');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,10 +110,6 @@ export class ApiService {
|
|||||||
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/contributors');
|
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/contributors');
|
||||||
}
|
}
|
||||||
|
|
||||||
checkDonation$(orderId: string): Observable<any[]> {
|
|
||||||
return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/donations/check?order_id=' + orderId);
|
|
||||||
}
|
|
||||||
|
|
||||||
getInitData$(): Observable<WebsocketResponse> {
|
getInitData$(): Observable<WebsocketResponse> {
|
||||||
return this.httpClient.get<WebsocketResponse>(this.apiBaseUrl + this.apiBasePath + '/api/v1/init-data');
|
return this.httpClient.get<WebsocketResponse>(this.apiBaseUrl + this.apiBasePath + '/api/v1/init-data');
|
||||||
}
|
}
|
||||||
@ -323,4 +317,13 @@ export class ApiService {
|
|||||||
(timestamp ? `?timestamp=${timestamp}` : '')
|
(timestamp ? `?timestamp=${timestamp}` : '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Services
|
||||||
|
*/
|
||||||
|
getNodeOwner$(publicKey: string) {
|
||||||
|
let params = new HttpParams()
|
||||||
|
.set('node_public_key', publicKey);
|
||||||
|
return this.httpClient.get<any>(`${SERVICES_API_PREFIX}/lightning/claim/current`, { params, observe: 'response' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@ export interface Env {
|
|||||||
TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
|
TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||||
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
|
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||||
HISTORICAL_PRICE: boolean;
|
HISTORICAL_PRICE: boolean;
|
||||||
|
ACCELERATOR: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultEnv: Env = {
|
const defaultEnv: Env = {
|
||||||
@ -77,6 +78,7 @@ const defaultEnv: Env = {
|
|||||||
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||||
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||||
'HISTORICAL_PRICE': true,
|
'HISTORICAL_PRICE': true,
|
||||||
|
'ACCELERATOR': false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<footer>
|
<footer>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row main">
|
<div class="row main">
|
||||||
<div class="offset-lg-1 col-lg-4 col align-self-center branding">
|
<div class="offset-lg-1 col-lg-4 col align-self-center branding mt-2">
|
||||||
<div class="main-logo">
|
<div class="main-logo">
|
||||||
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
<app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||||
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
<app-svg-images *ngIf="!officialMempoolSpace" name="mempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||||
@ -16,10 +16,12 @@
|
|||||||
<div class="selector">
|
<div class="selector">
|
||||||
<app-rate-unit-selector></app-rate-unit-selector>
|
<app-rate-unit-selector></app-rate-unit-selector>
|
||||||
</div>
|
</div>
|
||||||
<ng-template #temporaryHidden>
|
<div *ngIf="officialMempoolSpace && stateService.env.ACCELERATOR" class="cta">
|
||||||
<a *ngIf="officialMempoolSpace" class="cta btn btn-purple sponsor" [routerLink]="['/signup' | relativeUrl]">Support the Project</a>
|
<a class="btn btn-purple sponsor" [routerLink]="['/login' | relativeUrl]">
|
||||||
<p *ngIf="officialMempoolSpace && env.BASE_MODULE === 'mempool'" class="cta-secondary"><a [routerLink]="['/signin' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Sign In</a></p>
|
<span *ngIf="loggedIn" i18n="shared.my-account">My Account</span>
|
||||||
</ng-template>
|
<span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In / Sign Up</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6 col-md-10 offset-md-1 links outer">
|
<div class="col-lg-6 col-md-10 offset-md-1 links outer">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -22,7 +22,7 @@ footer .row.main .branding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
footer .row.main .branding > p {
|
footer .row.main .branding > p {
|
||||||
margin-bottom: 45px;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .row.main .branding .btn {
|
footer .row.main .branding .btn {
|
||||||
@ -35,11 +35,7 @@ footer .row.main .branding button.account {
|
|||||||
}
|
}
|
||||||
|
|
||||||
footer .row.main .branding .cta {
|
footer .row.main .branding .cta {
|
||||||
margin: 20px auto 25px auto;
|
margin: 25px auto 25px auto;
|
||||||
}
|
|
||||||
|
|
||||||
footer .row.main .branding .cta-secondary {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
footer .row.main .links.outer {
|
footer .row.main .links.outer {
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit, Inject, LOCALE_ID } from '@angular/core';
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, Inject, LOCALE_ID } from '@angular/core';
|
||||||
import { Observable, merge, of, Subject } from 'rxjs';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { Observable, merge, of, Subject, Subscription } from 'rxjs';
|
||||||
import { tap, takeUntil } from 'rxjs/operators';
|
import { tap, takeUntil } from 'rxjs/operators';
|
||||||
import { Env, StateService } from '../../../services/state.service';
|
import { Env, StateService } from '../../../services/state.service';
|
||||||
import { IBackendInfo } from '../../../interfaces/websocket.interface';
|
import { IBackendInfo } from '../../../interfaces/websocket.interface';
|
||||||
import { LanguageService } from '../../../services/language.service';
|
import { LanguageService } from '../../../services/language.service';
|
||||||
import { NavigationService } from '../../../services/navigation.service';
|
import { NavigationService } from '../../../services/navigation.service';
|
||||||
|
import { StorageService } from '../../../services/storage.service';
|
||||||
|
import { WebsocketService } from '../../../services/websocket.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-global-footer',
|
selector: 'app-global-footer',
|
||||||
@ -23,12 +26,19 @@ export class GlobalFooterComponent implements OnInit {
|
|||||||
network$: Observable<string>;
|
network$: Observable<string>;
|
||||||
networkPaths: { [network: string]: string };
|
networkPaths: { [network: string]: string };
|
||||||
currentNetwork = '';
|
currentNetwork = '';
|
||||||
|
loggedIn = false;
|
||||||
|
username = null;
|
||||||
|
urlSubscription: Subscription;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public stateService: StateService,
|
public stateService: StateService,
|
||||||
private languageService: LanguageService,
|
private languageService: LanguageService,
|
||||||
private navigationService: NavigationService,
|
private navigationService: NavigationService,
|
||||||
@Inject(LOCALE_ID) public locale: string,
|
@Inject(LOCALE_ID) public locale: string,
|
||||||
|
private storageService: StorageService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
private websocketService: WebsocketService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@ -46,11 +56,23 @@ export class GlobalFooterComponent implements OnInit {
|
|||||||
this.network$.pipe(takeUntil(this.destroy$)).subscribe((network) => {
|
this.network$.pipe(takeUntil(this.destroy$)).subscribe((network) => {
|
||||||
this.currentNetwork = network;
|
this.currentNetwork = network;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.urlSubscription = this.route.url.subscribe((url) => {
|
||||||
|
this.loggedIn = JSON.parse(this.storageService.getValue('auth')) !== null;
|
||||||
|
const auth = JSON.parse(this.storageService.getValue('auth'));
|
||||||
|
if (auth?.user?.username) {
|
||||||
|
this.username = auth.user.username;
|
||||||
|
} else {
|
||||||
|
this.username = null;
|
||||||
|
}
|
||||||
|
this.cd.markForCheck();
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.destroy$.next(true);
|
this.destroy$.next(true);
|
||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
|
this.urlSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
networkLink(network) {
|
networkLink(network) {
|
||||||
|
@ -219,6 +219,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||||||
AmountShortenerPipe,
|
AmountShortenerPipe,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
|
MasterPageComponent,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgbNavModule,
|
NgbNavModule,
|
||||||
|
1
frontend/src/resources/profile/unknown.svg
Normal file
1
frontend/src/resources/profile/unknown.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" data-v-4fa90e7f=""><path d="M14.33 7.17C13.588 7.058 12.807 7 12 7c-4.97 0-9 2.239-9 5 0 1.44 1.096 2.738 2.85 3.65l2.362-2.362a4 4 0 015.076-5.076l1.043-1.043zM11.23 15.926a4 4 0 004.695-4.695l2.648-2.647C20.078 9.478 21 10.68 21 12c0 2.761-4.03 5-9 5-.598 0-1.183-.032-1.749-.094l.98-.98zM17.793 5.207a1 1 0 111.414 1.414L6.48 19.35a1 1 0 11-1.414-1.414L17.793 5.207z"></path></svg>
|
After Width: | Height: | Size: 464 B |
Loading…
x
Reference in New Issue
Block a user