Merge pull request #3995 from mempool/mononaut/acceleration-viz
Acceleration visualization
This commit is contained in:
		
						commit
						86ffa48046
					
				@ -137,5 +137,9 @@
 | 
			
		||||
      "trusted",
 | 
			
		||||
      "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
 | 
			
		||||
  inputs: Array<number>
 | 
			
		||||
}
 | 
			
		||||
export interface ThreadAcceleration {
 | 
			
		||||
  uid: number
 | 
			
		||||
  delta: number
 | 
			
		||||
}
 | 
			
		||||
export class GbtGenerator {
 | 
			
		||||
  constructor()
 | 
			
		||||
  /**
 | 
			
		||||
@ -19,13 +23,13 @@ export class GbtGenerator {
 | 
			
		||||
   *
 | 
			
		||||
   * 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
 | 
			
		||||
   *
 | 
			
		||||
   * 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.
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
use crate::{
 | 
			
		||||
    u32_hasher_types::{u32hashset_new, U32HasherState},
 | 
			
		||||
    ThreadTransaction,
 | 
			
		||||
    ThreadTransaction, thread_acceleration::ThreadAcceleration,
 | 
			
		||||
};
 | 
			
		||||
use std::{
 | 
			
		||||
    cmp::Ordering,
 | 
			
		||||
@ -88,44 +88,49 @@ impl Ord for AuditTransaction {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[inline]
 | 
			
		||||
fn calc_fee_rate(fee: f64, vsize: f64) -> f64 {
 | 
			
		||||
    fee / (if vsize == 0.0 { 1.0 } else { vsize })
 | 
			
		||||
fn calc_fee_rate(fee: u64, vsize: f64) -> f64 {
 | 
			
		||||
    (fee as f64) / (if vsize == 0.0 { 1.0 } else { vsize })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
        let is_adjusted = tx.weight < (tx.sigops * 20);
 | 
			
		||||
        let sigop_adjusted_vsize = ((tx.weight + 3) / 4).max(tx.sigops * 5);
 | 
			
		||||
        let sigop_adjusted_weight = tx.weight.max(tx.sigops * 20);
 | 
			
		||||
        let effective_fee_per_vsize = if is_adjusted {
 | 
			
		||||
            calc_fee_rate(tx.fee, f64::from(sigop_adjusted_weight) / 4.0)
 | 
			
		||||
        let effective_fee_per_vsize = if is_adjusted || fee_delta > 0.0 {
 | 
			
		||||
            calc_fee_rate(fee, f64::from(sigop_adjusted_weight) / 4.0)
 | 
			
		||||
        } else {
 | 
			
		||||
            tx.effective_fee_per_vsize
 | 
			
		||||
        };
 | 
			
		||||
        Self {
 | 
			
		||||
            uid: tx.uid,
 | 
			
		||||
            order: tx.order,
 | 
			
		||||
            fee: tx.fee as u64,
 | 
			
		||||
            fee,
 | 
			
		||||
            weight: tx.weight,
 | 
			
		||||
            sigop_adjusted_weight,
 | 
			
		||||
            sigop_adjusted_vsize,
 | 
			
		||||
            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,
 | 
			
		||||
            dependency_rate: f64::INFINITY,
 | 
			
		||||
            inputs: tx.inputs.clone(),
 | 
			
		||||
            relatives_set_flag: false,
 | 
			
		||||
            ancestors: u32hashset_new(),
 | 
			
		||||
            children: u32hashset_new(),
 | 
			
		||||
            ancestor_fee: tx.fee as u64,
 | 
			
		||||
            ancestor_fee: fee,
 | 
			
		||||
            ancestor_sigop_adjusted_weight: sigop_adjusted_weight,
 | 
			
		||||
            ancestor_sigop_adjusted_vsize: sigop_adjusted_vsize,
 | 
			
		||||
            ancestor_sigops: tx.sigops,
 | 
			
		||||
            score: 0.0,
 | 
			
		||||
            used: 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
 | 
			
		||||
        // the smaller of the two. If either side is NaN, the other side is returned.
 | 
			
		||||
        self.dependency_rate.min(calc_fee_rate(
 | 
			
		||||
            self.ancestor_fee as f64,
 | 
			
		||||
            self.ancestor_fee,
 | 
			
		||||
            f64::from(self.ancestor_sigop_adjusted_weight) / 4.0,
 | 
			
		||||
        ))
 | 
			
		||||
    }
 | 
			
		||||
@ -172,7 +177,7 @@ impl AuditTransaction {
 | 
			
		||||
    #[inline]
 | 
			
		||||
    fn calc_new_score(&mut self) {
 | 
			
		||||
        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),
 | 
			
		||||
        ));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ use tracing::{info, trace};
 | 
			
		||||
use crate::{
 | 
			
		||||
    audit_transaction::{partial_cmp_uid_score, AuditTransaction},
 | 
			
		||||
    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;
 | 
			
		||||
@ -53,7 +53,13 @@ impl Ord for TxPriority {
 | 
			
		||||
// TODO: Make gbt smaller to fix these lints.
 | 
			
		||||
#[allow(clippy::too_many_lines)]
 | 
			
		||||
#[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 mut audit_pool: AuditPool = Vec::with_capacity(max_uid + 1);
 | 
			
		||||
    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");
 | 
			
		||||
    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
 | 
			
		||||
        audit_pool[*uid as usize] = Some(ManuallyDrop::new(audit_tx));
 | 
			
		||||
        mempool_stack.push(*uid);
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@
 | 
			
		||||
use napi::bindgen_prelude::Result;
 | 
			
		||||
use napi_derive::napi;
 | 
			
		||||
use thread_transaction::ThreadTransaction;
 | 
			
		||||
use thread_acceleration::ThreadAcceleration;
 | 
			
		||||
use tracing::{debug, info, trace};
 | 
			
		||||
use tracing_log::LogTracer;
 | 
			
		||||
use tracing_subscriber::{EnvFilter, FmtSubscriber};
 | 
			
		||||
@ -19,6 +20,7 @@ use std::sync::{Arc, Mutex};
 | 
			
		||||
mod audit_transaction;
 | 
			
		||||
mod gbt;
 | 
			
		||||
mod thread_transaction;
 | 
			
		||||
mod thread_acceleration;
 | 
			
		||||
mod u32_hasher_types;
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
    #[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);
 | 
			
		||||
        run_task(
 | 
			
		||||
            Arc::clone(&self.thread_transactions),
 | 
			
		||||
            accelerations,
 | 
			
		||||
            max_uid as usize,
 | 
			
		||||
            move |map| {
 | 
			
		||||
                for tx in mempool {
 | 
			
		||||
@ -96,11 +99,13 @@ impl GbtGenerator {
 | 
			
		||||
        &self,
 | 
			
		||||
        new_txs: Vec<ThreadTransaction>,
 | 
			
		||||
        remove_txs: Vec<u32>,
 | 
			
		||||
        accelerations: Vec<ThreadAcceleration>,
 | 
			
		||||
        max_uid: u32,
 | 
			
		||||
    ) -> Result<GbtResult> {
 | 
			
		||||
        trace!("update: Current State {:#?}", self.thread_transactions);
 | 
			
		||||
        run_task(
 | 
			
		||||
            Arc::clone(&self.thread_transactions),
 | 
			
		||||
            accelerations,
 | 
			
		||||
            max_uid as usize,
 | 
			
		||||
            move |map| {
 | 
			
		||||
                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)
 | 
			
		||||
async fn run_task<F>(
 | 
			
		||||
    thread_transactions: Arc<Mutex<ThreadTransactionsMap>>,
 | 
			
		||||
    accelerations: Vec<ThreadAcceleration>,
 | 
			
		||||
    max_uid: usize,
 | 
			
		||||
    callback: F,
 | 
			
		||||
) -> Result<GbtResult>
 | 
			
		||||
@ -159,7 +165,7 @@ where
 | 
			
		||||
        callback(&mut map);
 | 
			
		||||
 | 
			
		||||
        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());
 | 
			
		||||
 | 
			
		||||
        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,
 | 
			
		||||
    "SERVERS": []
 | 
			
		||||
  },
 | 
			
		||||
  "MEMPOOL_SERVICES": {
 | 
			
		||||
    "API": "",
 | 
			
		||||
    "ACCELERATIONS": false
 | 
			
		||||
  },
 | 
			
		||||
  "REDIS": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "UNIX_SOCKET_PATH": "/tmp/redis.sock"
 | 
			
		||||
 | 
			
		||||
@ -129,6 +129,11 @@ describe('Mempool Backend Config', () => {
 | 
			
		||||
        SERVERS: []
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(config.MEMPOOL_SERVICES).toStrictEqual({
 | 
			
		||||
        API: "",
 | 
			
		||||
        ACCELERATIONS: false,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(config.REDIS).toStrictEqual({
 | 
			
		||||
        ENABLED: false,
 | 
			
		||||
        UNIX_SOCKET_PATH: ''
 | 
			
		||||
@ -167,6 +172,8 @@ describe('Mempool Backend Config', () => {
 | 
			
		||||
 | 
			
		||||
      expect(config.EXTERNAL_DATA_SERVER).toStrictEqual(fixture.EXTERNAL_DATA_SERVER);
 | 
			
		||||
 | 
			
		||||
      expect(config.MEMPOOL_SERVICES).toStrictEqual(fixture.MEMPOOL_SERVICES);
 | 
			
		||||
 | 
			
		||||
      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 () => {
 | 
			
		||||
    const rustGbt = new GbtGenerator();
 | 
			
		||||
    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 => {
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
class Audit {
 | 
			
		||||
  auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended })
 | 
			
		||||
   : { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], score: number, similarity: number } {
 | 
			
		||||
  auditBlock(transactions: MempoolTransactionExtended[], projectedBlocks: MempoolBlockWithTransactions[], mempool: { [txId: string]: MempoolTransactionExtended }, useAccelerations: boolean = false)
 | 
			
		||||
   : { censored: string[], added: string[], fresh: string[], sigop: string[], fullrbf: string[], accelerated: string[], score: number, similarity: number } {
 | 
			
		||||
    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 added: string[] = []; // present in mined block, not in template
 | 
			
		||||
    const fresh: string[] = []; // missing, but firstSeen or lastBoosted within PROPAGATION_MARGIN
 | 
			
		||||
    const rbf: string[] = []; // either missing or present, and either part of a full-rbf replacement, or a conflict with the mined block
 | 
			
		||||
    const accelerated: string[] = []; // prioritized by the mempool accelerator
 | 
			
		||||
    const isCensored = {}; // missing, without excuse
 | 
			
		||||
    const isDisplaced = {};
 | 
			
		||||
    let displacedWeight = 0;
 | 
			
		||||
@ -28,6 +29,9 @@ class Audit {
 | 
			
		||||
    const now = Math.round((Date.now() / 1000));
 | 
			
		||||
    for (const tx of transactions) {
 | 
			
		||||
      inBlock[tx.txid] = tx;
 | 
			
		||||
      if (mempool[tx.txid] && mempool[tx.txid].acceleration) {
 | 
			
		||||
        accelerated.push(tx.txid);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // coinbase is always expected
 | 
			
		||||
    if (transactions[0]) {
 | 
			
		||||
@ -149,6 +153,7 @@ class Audit {
 | 
			
		||||
      fresh,
 | 
			
		||||
      sigop: [],
 | 
			
		||||
      fullrbf: rbf,
 | 
			
		||||
      accelerated,
 | 
			
		||||
      score,
 | 
			
		||||
      similarity,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@ -214,6 +214,7 @@ class BitcoinRoutes {
 | 
			
		||||
          effectiveFeePerVsize: tx.effectiveFeePerVsize || null,
 | 
			
		||||
          sigops: tx.sigops,
 | 
			
		||||
          adjustedVsize: tx.adjustedVsize,
 | 
			
		||||
          acceleration: tx.acceleration
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -111,6 +111,7 @@ export class Common {
 | 
			
		||||
      fee: tx.fee || 0,
 | 
			
		||||
      vsize: tx.weight / 4,
 | 
			
		||||
      value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
 | 
			
		||||
      acc: tx.acceleration || undefined,
 | 
			
		||||
      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);
 | 
			
		||||
 | 
			
		||||
    let weightCount = 0;
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ import cpfpRepository from '../repositories/CpfpRepository';
 | 
			
		||||
import { RowDataPacket } from 'mysql2';
 | 
			
		||||
 | 
			
		||||
class DatabaseMigration {
 | 
			
		||||
  private static currentVersion = 64;
 | 
			
		||||
  private static currentVersion = 65;
 | 
			
		||||
  private queryTimeout = 3600_000;
 | 
			
		||||
  private statisticsAddedIndexed = false;
 | 
			
		||||
  private uniqueLogs: string[] = [];
 | 
			
		||||
@ -548,6 +548,11 @@ class DatabaseMigration {
 | 
			
		||||
      await this.$executeQuery('ALTER TABLE `nodes` ADD features text NULL');
 | 
			
		||||
      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 { 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 config from '../config';
 | 
			
		||||
import { Worker } from 'worker_threads';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import mempool from './mempool';
 | 
			
		||||
 | 
			
		||||
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++) {
 | 
			
		||||
      let added: TransactionStripped[] = [];
 | 
			
		||||
      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]) {
 | 
			
		||||
        added = mempoolBlocks[i].transactions;
 | 
			
		||||
      } else if (!mempoolBlocks[i] && prevBlocks[i]) {
 | 
			
		||||
@ -192,8 +193,8 @@ class MempoolBlocks {
 | 
			
		||||
        mempoolBlocks[i].transactions.forEach(tx => {
 | 
			
		||||
          if (!prevIds[tx.txid]) {
 | 
			
		||||
            added.push(tx);
 | 
			
		||||
          } else if (tx.rate !== prevIds[tx.txid].rate) {
 | 
			
		||||
            changed.push({ txid: tx.txid, rate: tx.rate });
 | 
			
		||||
          } else if (tx.rate !== prevIds[tx.txid].rate || tx.acc !== prevIds[tx.txid].acc) {
 | 
			
		||||
            changed.push({ txid: tx.txid, rate: tx.rate, acc: tx.acc });
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
@ -206,14 +207,19 @@ class MempoolBlocks {
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
    // reset mempool short ids
 | 
			
		||||
    this.resetUids();
 | 
			
		||||
    for (const tx of Object.values(newMempool)) {
 | 
			
		||||
      this.setUid(tx);
 | 
			
		||||
    if (saveResults) {
 | 
			
		||||
      this.resetUids();
 | 
			
		||||
    }
 | 
			
		||||
    // 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
 | 
			
		||||
    // 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) {
 | 
			
		||||
        const stripped = {
 | 
			
		||||
          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),
 | 
			
		||||
          sigops: entry.sigops,
 | 
			
		||||
          feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
 | 
			
		||||
@ -262,7 +268,7 @@ class MempoolBlocks {
 | 
			
		||||
      // clean up thread error listener
 | 
			
		||||
      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`);
 | 
			
		||||
 | 
			
		||||
@ -273,25 +279,29 @@ class 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) {
 | 
			
		||||
      // need to reset the worker
 | 
			
		||||
      await this.$makeBlockTemplates(newMempool, saveResults);
 | 
			
		||||
      await this.$makeBlockTemplates(newMempool, saveResults, useAccelerations);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
    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
 | 
			
		||||
    // 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 {
 | 
			
		||||
        uid: entry.uid || 0,
 | 
			
		||||
        fee: entry.fee,
 | 
			
		||||
        fee: entry.fee + (useAccelerations ? (accelerations[entry.txid]?.feeDelta || 0) : 0),
 | 
			
		||||
        weight: (entry.adjustedVsize * 4),
 | 
			
		||||
        sigops: entry.sigops,
 | 
			
		||||
        feePerVsize: entry.adjustedFeePerVsize || entry.feePerVsize,
 | 
			
		||||
@ -318,7 +328,7 @@ class MempoolBlocks {
 | 
			
		||||
      // clean up thread error listener
 | 
			
		||||
      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`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -330,7 +340,8 @@ class MempoolBlocks {
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
    // 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[];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
    const rustGbt = saveResults ? this.rustGbtGenerator : new GbtGenerator();
 | 
			
		||||
    try {
 | 
			
		||||
      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) {
 | 
			
		||||
        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`);
 | 
			
		||||
      return processed;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
@ -367,20 +387,20 @@ class MempoolBlocks {
 | 
			
		||||
    return this.mempoolBlocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $oneOffRustBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }): Promise<MempoolBlockWithTransactions[]> {
 | 
			
		||||
    return this.$rustMakeBlockTemplates(newMempool, false);
 | 
			
		||||
  public async $oneOffRustBlockTemplates(newMempool: { [txid: string]: MempoolTransactionExtended }, useAccelerations: boolean, accelerationPool?: number): Promise<MempoolBlockWithTransactions[]> {
 | 
			
		||||
    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
 | 
			
		||||
    // 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)) {
 | 
			
		||||
      this.resetRustGbt();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this.rustInitialized) {
 | 
			
		||||
      // need to reset the worker
 | 
			
		||||
      await this.$rustMakeBlockTemplates(newMempool, true);
 | 
			
		||||
      return;
 | 
			
		||||
      return this.$rustMakeBlockTemplates(newMempool, true, useAccelerations, accelerationPool);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 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
 | 
			
		||||
    try {
 | 
			
		||||
      const { blocks, blockWeights, rates, clusters } = this.convertNapiResultTxids(
 | 
			
		||||
        await this.rustGbtGenerator.update(
 | 
			
		||||
          added as RustThreadTransaction[],
 | 
			
		||||
          removedUids,
 | 
			
		||||
          convertedAccelerations as RustThreadAcceleration[],
 | 
			
		||||
          this.nextUid,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
@ -407,17 +437,19 @@ class MempoolBlocks {
 | 
			
		||||
      if (mempoolSize !== resultMempoolSize) {
 | 
			
		||||
        throw new Error('GBT returned wrong number of transactions, cache is probably out of sync');
 | 
			
		||||
      } 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) {
 | 
			
		||||
      logger.err('RUST updateBlockTemplates failed. ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      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) {
 | 
			
		||||
      if (txid in mempool) {
 | 
			
		||||
        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;
 | 
			
		||||
    // update this thread's mempool with the results
 | 
			
		||||
    let mempoolTx: MempoolTransactionExtended;
 | 
			
		||||
@ -496,6 +530,17 @@ class MempoolBlocks {
 | 
			
		||||
            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
 | 
			
		||||
          if (hasBlockStack && blockIndex === lastBlockIndex && feeStatsCalculator) {
 | 
			
		||||
            feeStatsCalculator.processNext(mempoolTx);
 | 
			
		||||
@ -532,7 +577,7 @@ class MempoolBlocks {
 | 
			
		||||
 | 
			
		||||
  private dataToMempoolBlocks(transactionIds: string[], transactions: MempoolTransactionExtended[], totalSize: number, totalWeight: number, totalFees: number, feeStats?: EffectiveFeeStats ): MempoolBlockWithTransactions {
 | 
			
		||||
    if (!feeStats) {
 | 
			
		||||
      feeStats = Common.calcEffectiveFeeStatistics(transactions);
 | 
			
		||||
      feeStats = Common.calcEffectiveFeeStatistics(transactions.filter(tx => !tx.acceleration));
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      blockSize: totalSize,
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ import loadingIndicators from './loading-indicators';
 | 
			
		||||
import bitcoinClient from './bitcoin/bitcoin-client';
 | 
			
		||||
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
 | 
			
		||||
import rbfCache from './rbf-cache';
 | 
			
		||||
import accelerationApi, { Acceleration } from './services/acceleration';
 | 
			
		||||
import redisCache from './redis-cache';
 | 
			
		||||
 | 
			
		||||
class Mempool {
 | 
			
		||||
@ -19,9 +20,11 @@ class Mempool {
 | 
			
		||||
  private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
 | 
			
		||||
                                                    maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
 | 
			
		||||
  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[],
 | 
			
		||||
    deletedTransactions: MempoolTransactionExtended[]) => Promise<void>) | undefined;
 | 
			
		||||
    deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>) | undefined;
 | 
			
		||||
 | 
			
		||||
  private accelerations: { [txId: string]: Acceleration } = {};
 | 
			
		||||
 | 
			
		||||
  private txPerSecondArray: number[] = [];
 | 
			
		||||
  private txPerSecond: number = 0;
 | 
			
		||||
@ -66,12 +69,12 @@ class Mempool {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; },
 | 
			
		||||
    newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => void): void {
 | 
			
		||||
    newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => void): void {
 | 
			
		||||
    this.mempoolChangedCallback = fn;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setAsyncMempoolChangedCallback(fn: (newMempool: { [txId: string]: MempoolTransactionExtended; }, mempoolSize: number,
 | 
			
		||||
    newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[]) => Promise<void>): void {
 | 
			
		||||
    newTransactions: MempoolTransactionExtended[], deletedTransactions: MempoolTransactionExtended[], accelerationDelta: string[]) => Promise<void>): void {
 | 
			
		||||
    this.$asyncMempoolChangedCallback = fn;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -107,10 +110,10 @@ class Mempool {
 | 
			
		||||
      logger.debug(`Finished migrating cache transactions in ${((Date.now() - redisTimer) / 1000).toFixed(2)} seconds`);
 | 
			
		||||
    }
 | 
			
		||||
    if (this.mempoolChangedCallback) {
 | 
			
		||||
      this.mempoolChangedCallback(this.mempoolCache, [], []);
 | 
			
		||||
      this.mempoolChangedCallback(this.mempoolCache, [], [], []);
 | 
			
		||||
    }
 | 
			
		||||
    if (this.$asyncMempoolChangedCallback) {
 | 
			
		||||
      await this.$asyncMempoolChangedCallback(this.mempoolCache, count, [], []);
 | 
			
		||||
      await this.$asyncMempoolChangedCallback(this.mempoolCache, count, [], [], []);
 | 
			
		||||
    }
 | 
			
		||||
    this.addToSpendMap(Object.values(this.mempoolCache));
 | 
			
		||||
  }
 | 
			
		||||
@ -321,14 +324,19 @@ class Mempool {
 | 
			
		||||
    const newTransactionsStripped = newTransactions.map((tx) => Common.stripTransaction(tx));
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
    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)) {
 | 
			
		||||
      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');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -352,6 +360,70 @@ class Mempool {
 | 
			
		||||
    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() {
 | 
			
		||||
    const state: any = {
 | 
			
		||||
      start: Date.now(),
 | 
			
		||||
 | 
			
		||||
@ -107,6 +107,7 @@ class Mining {
 | 
			
		||||
        slug: poolInfo.slug,
 | 
			
		||||
        avgMatchRate: poolInfo.avgMatchRate !== null ? Math.round(100 * poolInfo.avgMatchRate) / 100 : null,
 | 
			
		||||
        avgFeeDelta: poolInfo.avgFeeDelta,
 | 
			
		||||
        poolUniqueId: poolInfo.poolUniqueId
 | 
			
		||||
      };
 | 
			
		||||
      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 priceUpdater from '../tasks/price-updater';
 | 
			
		||||
import { ApiPrice } from '../repositories/PricesRepository';
 | 
			
		||||
import accelerationApi from './services/acceleration';
 | 
			
		||||
import mempool from './mempool';
 | 
			
		||||
 | 
			
		||||
// valid 'want' subscriptions
 | 
			
		||||
const wantable = [
 | 
			
		||||
@ -172,9 +174,15 @@ class WebsocketHandler {
 | 
			
		||||
              }
 | 
			
		||||
              const tx = memPool.getMempool()[trackTxid];
 | 
			
		||||
              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({
 | 
			
		||||
                  txid: trackTxid,
 | 
			
		||||
                  position: tx.position,
 | 
			
		||||
                  position
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
@ -390,7 +398,7 @@ class WebsocketHandler {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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) {
 | 
			
		||||
      throw new Error('WebSocket.Server is not set');
 | 
			
		||||
    }
 | 
			
		||||
@ -399,9 +407,9 @@ class WebsocketHandler {
 | 
			
		||||
 | 
			
		||||
    if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
 | 
			
		||||
      if (config.MEMPOOL.RUST_GBT) {
 | 
			
		||||
        await mempoolBlocks.$rustUpdateBlockTemplates(newMempool, mempoolSize, newTransactions, deletedTransactions);
 | 
			
		||||
        await mempoolBlocks.$rustUpdateBlockTemplates(newMempool, mempoolSize, newTransactions, deletedTransactions, config.MEMPOOL_SERVICES.ACCELERATIONS);
 | 
			
		||||
      } else {
 | 
			
		||||
        await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true);
 | 
			
		||||
        await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, accelerationDelta, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      mempoolBlocks.updateMempoolBlocks(newMempool, true);
 | 
			
		||||
@ -647,7 +655,10 @@ class WebsocketHandler {
 | 
			
		||||
        if (mempoolTx && mempoolTx.position) {
 | 
			
		||||
          response['txPosition'] = JSON.stringify({
 | 
			
		||||
            txid: trackTxid,
 | 
			
		||||
            position: mempoolTx.position,
 | 
			
		||||
            position: {
 | 
			
		||||
              ...mempoolTx.position,
 | 
			
		||||
              accelerated: mempoolTx.acceleration || undefined,
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
@ -695,6 +706,7 @@ class WebsocketHandler {
 | 
			
		||||
    if (config.MEMPOOL.AUDIT && memPool.isInSync()) {
 | 
			
		||||
      let projectedBlocks;
 | 
			
		||||
      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
 | 
			
		||||
      // 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;
 | 
			
		||||
@ -702,19 +714,27 @@ class WebsocketHandler {
 | 
			
		||||
        auditMempool = deepClone(_memPool);
 | 
			
		||||
        if (config.MEMPOOL.ADVANCED_GBT_AUDIT) {
 | 
			
		||||
          if (config.MEMPOOL.RUST_GBT) {
 | 
			
		||||
            projectedBlocks = await mempoolBlocks.$oneOffRustBlockTemplates(auditMempool);
 | 
			
		||||
            projectedBlocks = await mempoolBlocks.$oneOffRustBlockTemplates(auditMempool, isAccelerated, block.extras.pool.id);
 | 
			
		||||
          } else {
 | 
			
		||||
            projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false);
 | 
			
		||||
            projectedBlocks = await mempoolBlocks.$makeBlockTemplates(auditMempool, false, isAccelerated, block.extras.pool.id);
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          projectedBlocks = mempoolBlocks.updateMempoolBlocks(auditMempool, false);
 | 
			
		||||
        }
 | 
			
		||||
      } 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()) {
 | 
			
		||||
        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 stripped = projectedBlocks[0]?.transactions ? projectedBlocks[0].transactions : [];
 | 
			
		||||
@ -743,6 +763,7 @@ class WebsocketHandler {
 | 
			
		||||
          freshTxs: fresh,
 | 
			
		||||
          sigopTxs: sigop,
 | 
			
		||||
          fullrbfTxs: fullrbf,
 | 
			
		||||
          acceleratedTxs: accelerated,
 | 
			
		||||
          matchRate: matchRate,
 | 
			
		||||
          expectedFees: totalFees,
 | 
			
		||||
          expectedWeight: totalWeight,
 | 
			
		||||
@ -770,9 +791,9 @@ class WebsocketHandler {
 | 
			
		||||
 | 
			
		||||
    if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
 | 
			
		||||
      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 {
 | 
			
		||||
        await mempoolBlocks.$makeBlockTemplates(_memPool, true);
 | 
			
		||||
        await mempoolBlocks.$makeBlockTemplates(_memPool, true, config.MEMPOOL_SERVICES.ACCELERATIONS);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      mempoolBlocks.updateMempoolBlocks(_memPool, true);
 | 
			
		||||
@ -836,7 +857,10 @@ class WebsocketHandler {
 | 
			
		||||
          if (mempoolTx && mempoolTx.position) {
 | 
			
		||||
            response['txPosition'] = JSON.stringify({
 | 
			
		||||
              txid: trackTxid,
 | 
			
		||||
              position: mempoolTx.position,
 | 
			
		||||
              position: {
 | 
			
		||||
                ...mempoolTx.position,
 | 
			
		||||
                accelerated: mempoolTx.acceleration || undefined,
 | 
			
		||||
              }
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -139,6 +139,10 @@ interface IConfig {
 | 
			
		||||
    AUDIT_START_HEIGHT: number;
 | 
			
		||||
    SERVERS: string[];
 | 
			
		||||
  },
 | 
			
		||||
  MEMPOOL_SERVICES: {
 | 
			
		||||
    API: string;
 | 
			
		||||
    ACCELERATIONS: boolean;
 | 
			
		||||
  },
 | 
			
		||||
  REDIS: {
 | 
			
		||||
    ENABLED: boolean;
 | 
			
		||||
    UNIX_SOCKET_PATH: string;
 | 
			
		||||
@ -282,6 +286,10 @@ const defaults: IConfig = {
 | 
			
		||||
    'AUDIT_START_HEIGHT': 774000,
 | 
			
		||||
    'SERVERS': [],
 | 
			
		||||
  },
 | 
			
		||||
  'MEMPOOL_SERVICES': {
 | 
			
		||||
    'API': '',
 | 
			
		||||
    'ACCELERATIONS': false,
 | 
			
		||||
  },
 | 
			
		||||
  'REDIS': {
 | 
			
		||||
    'ENABLED': false,
 | 
			
		||||
    'UNIX_SOCKET_PATH': '',
 | 
			
		||||
@ -306,6 +314,7 @@ class Config implements IConfig {
 | 
			
		||||
  EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
 | 
			
		||||
  MAXMIND: IConfig['MAXMIND'];
 | 
			
		||||
  REPLICATION: IConfig['REPLICATION'];
 | 
			
		||||
  MEMPOOL_SERVICES: IConfig['MEMPOOL_SERVICES'];
 | 
			
		||||
  REDIS: IConfig['REDIS'];
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
@ -327,6 +336,7 @@ class Config implements IConfig {
 | 
			
		||||
    this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
 | 
			
		||||
    this.MAXMIND = configs.MAXMIND;
 | 
			
		||||
    this.REPLICATION = configs.REPLICATION;
 | 
			
		||||
    this.MEMPOOL_SERVICES = configs.MEMPOOL_SERVICES;
 | 
			
		||||
    this.REDIS = configs.REDIS;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ export interface PoolInfo {
 | 
			
		||||
  slug: string;
 | 
			
		||||
  avgMatchRate: number | null;
 | 
			
		||||
  avgFeeDelta: number | null;
 | 
			
		||||
  poolUniqueId: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PoolStats extends PoolInfo {
 | 
			
		||||
@ -36,6 +37,7 @@ export interface BlockAudit {
 | 
			
		||||
  sigopTxs: string[],
 | 
			
		||||
  fullrbfTxs: string[],
 | 
			
		||||
  addedTxs: string[],
 | 
			
		||||
  acceleratedTxs: string[],
 | 
			
		||||
  matchRate: number,
 | 
			
		||||
  expectedFees?: number,
 | 
			
		||||
  expectedWeight?: number,
 | 
			
		||||
@ -91,6 +93,7 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
 | 
			
		||||
    block: number,
 | 
			
		||||
    vsize: number,
 | 
			
		||||
  };
 | 
			
		||||
  acceleration?: boolean;
 | 
			
		||||
  uid?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -182,6 +185,7 @@ export interface TransactionStripped {
 | 
			
		||||
  fee: number;
 | 
			
		||||
  vsize: number;
 | 
			
		||||
  value: number;
 | 
			
		||||
  acc?: boolean;
 | 
			
		||||
  rate?: number; // effective fee rate
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -116,6 +116,7 @@ class AuditReplication {
 | 
			
		||||
      freshTxs: auditSummary.freshTxs || [],
 | 
			
		||||
      sigopTxs: auditSummary.sigopTxs || [],
 | 
			
		||||
      fullrbfTxs: auditSummary.fullrbfTxs || [],
 | 
			
		||||
      acceleratedTxs: auditSummary.acceleratedTxs || [],
 | 
			
		||||
      matchRate: auditSummary.matchRate,
 | 
			
		||||
      expectedFees: auditSummary.expectedFees,
 | 
			
		||||
      expectedWeight: auditSummary.expectedWeight,
 | 
			
		||||
 | 
			
		||||
@ -6,9 +6,9 @@ import { BlockAudit, AuditScore } from '../mempool.interfaces';
 | 
			
		||||
class BlocksAuditRepositories {
 | 
			
		||||
  public async $saveAudit(audit: BlockAudit): Promise<void> {
 | 
			
		||||
    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)
 | 
			
		||||
        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]);
 | 
			
		||||
      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),
 | 
			
		||||
          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) {
 | 
			
		||||
      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`);
 | 
			
		||||
@ -69,6 +69,7 @@ class BlocksAuditRepositories {
 | 
			
		||||
        fresh_txs as freshTxs,
 | 
			
		||||
        sigop_txs as sigopTxs,
 | 
			
		||||
        fullrbf_txs as fullrbfTxs,
 | 
			
		||||
        accelerated_txs as acceleratedTxs,
 | 
			
		||||
        match_rate as matchRate,
 | 
			
		||||
        expected_fees as expectedFees,
 | 
			
		||||
        expected_weight as expectedWeight
 | 
			
		||||
@ -83,6 +84,7 @@ class BlocksAuditRepositories {
 | 
			
		||||
        rows[0].freshTxs = JSON.parse(rows[0].freshTxs);
 | 
			
		||||
        rows[0].sigopTxs = JSON.parse(rows[0].sigopTxs);
 | 
			
		||||
        rows[0].fullrbfTxs = JSON.parse(rows[0].fullrbfTxs);
 | 
			
		||||
        rows[0].acceleratedTxs = JSON.parse(rows[0].acceleratedTxs);
 | 
			
		||||
        rows[0].template = JSON.parse(rows[0].template);
 | 
			
		||||
 | 
			
		||||
        return rows[0];
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,8 @@ class PoolsRepository {
 | 
			
		||||
          pools.link AS link,
 | 
			
		||||
          slug,
 | 
			
		||||
          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
 | 
			
		||||
      JOIN pools on pools.id = pool_id
 | 
			
		||||
      LEFT JOIN blocks_audits ON blocks_audits.height = blocks.height
 | 
			
		||||
 | 
			
		||||
@ -135,6 +135,10 @@
 | 
			
		||||
    "AUDIT_START_HEIGHT": __REPLICATION_AUDIT_START_HEIGHT__,
 | 
			
		||||
    "SERVERS": __REPLICATION_SERVERS__
 | 
			
		||||
  },
 | 
			
		||||
  "MEMPOOL_SERVICES": {
 | 
			
		||||
    "API": "__MEMPOOL_SERVICES_API__",
 | 
			
		||||
    "ACCELERATIONS": __MEMPOOL_SERVICES_ACCELERATIONS__
 | 
			
		||||
  },
 | 
			
		||||
  "REDIS": {
 | 
			
		||||
    "ENABLED": __REDIS_ENABLED__,
 | 
			
		||||
    "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_SERVERS__=${REPLICATION_SERVERS:=[]}
 | 
			
		||||
 | 
			
		||||
# MEMPOOL_SERVICES
 | 
			
		||||
__MEMPOOL_SERVICES_API__=${MEMPOOL_SERVICES_API:=""}
 | 
			
		||||
__MEMPOOL_SERVICES_ACCELERATIONS__=${MEMPOOL_SERVICES_ACCELERATIONS:=false}
 | 
			
		||||
 | 
			
		||||
# REDIS
 | 
			
		||||
__REDIS_ENABLED__=${REDIS_ENABLED:=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_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
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
@ -22,5 +22,6 @@
 | 
			
		||||
  "TESTNET_BLOCK_AUDIT_START_HEIGHT": 0,
 | 
			
		||||
  "SIGNET_BLOCK_AUDIT_START_HEIGHT": 0,
 | 
			
		||||
  "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">
 | 
			
		||||
  </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">
 | 
			
		||||
    <h3 i18n="about.sponsors.enterprise.withRocket">Enterprise Sponsors 🚀</h3>
 | 
			
		||||
    <div class="wrapper">
 | 
			
		||||
@ -191,16 +199,41 @@
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="community-sponsor" id="community-sponsors">
 | 
			
		||||
    <h3 i18n="about.sponsors.withHeart">Community Sponsors ❤️</h3>
 | 
			
		||||
  <ng-container *ngIf="officialMempoolSpace">
 | 
			
		||||
    <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">
 | 
			
		||||
      <ng-container *ngIf="sponsors$ | async as sponsors; else loadingSponsors">
 | 
			
		||||
        <ng-template ngFor let-sponsor [ngForOf]="sponsors">
 | 
			
		||||
          <a [href]="'https://twitter.com/' + sponsor.handle" target="_blank" rel="sponsored" [title]="sponsor.handle">
 | 
			
		||||
            <img class="image" [src]="'/api/v1/donations/images/' + sponsor.handle" />
 | 
			
		||||
          </a>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
      <ng-container *ngIf="ogs$ | async as ogs; else loadingSponsors">
 | 
			
		||||
        <a *ngFor="let ogSponsor of ogs" [href]="'https://twitter.com/' + ogSponsor.handle" target="_blank" rel="sponsored" [title]="ogSponsor.handle">
 | 
			
		||||
          <img class="image" [src]="'/api/v1/donations/images/' + ogSponsor.handle" onError="this.src = '/resources/profile/unknown.svg'; this.className = 'image unknown'"/>
 | 
			
		||||
        </a>
 | 
			
		||||
      </ng-container>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
@ -340,7 +373,7 @@
 | 
			
		||||
      <div class="wrapper">
 | 
			
		||||
        <ng-template ngFor let-translator [ngForOf]="translators">
 | 
			
		||||
          <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>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
      </div>
 | 
			
		||||
@ -354,7 +387,7 @@
 | 
			
		||||
      <div class="wrapper">
 | 
			
		||||
        <ng-template ngFor let-contributor [ngForOf]="contributors.regular">
 | 
			
		||||
          <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>
 | 
			
		||||
          </a>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
@ -366,7 +399,7 @@
 | 
			
		||||
      <div class="wrapper">
 | 
			
		||||
        <ng-template ngFor let-contributor [ngForOf]="contributors.core">
 | 
			
		||||
          <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>
 | 
			
		||||
          </a>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,9 @@
 | 
			
		||||
    margin: 25px;
 | 
			
		||||
    line-height: 32px;
 | 
			
		||||
  }
 | 
			
		||||
  .unknown {
 | 
			
		||||
    border: 1px solid #b4b4b4;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .image.not-rounded {
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import { Observable } from 'rxjs';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { IBackendInfo } from '../../interfaces/websocket.interface';
 | 
			
		||||
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 { DOCUMENT } from '@angular/common';
 | 
			
		||||
 | 
			
		||||
@ -19,14 +19,16 @@ import { DOCUMENT } from '@angular/common';
 | 
			
		||||
export class AboutComponent implements OnInit {
 | 
			
		||||
  @ViewChild('promoVideo') promoVideo: ElementRef;
 | 
			
		||||
  backendInfo$: Observable<IBackendInfo>;
 | 
			
		||||
  sponsors$: Observable<any>;
 | 
			
		||||
  translators$: Observable<ITranslators>;
 | 
			
		||||
  allContributors$: Observable<any>;
 | 
			
		||||
  frontendGitCommitHash = this.stateService.env.GIT_COMMIT_HASH;
 | 
			
		||||
  packetJsonVersion = this.stateService.env.PACKAGE_JSON_VERSION;
 | 
			
		||||
  officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
 | 
			
		||||
  showNavigateToSponsor = false;
 | 
			
		||||
 | 
			
		||||
  profiles$: Observable<any>;
 | 
			
		||||
  translators$: Observable<ITranslators>;
 | 
			
		||||
  allContributors$: Observable<any>;
 | 
			
		||||
  ogs$: Observable<any>;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
@ -43,10 +45,13 @@ export class AboutComponent implements OnInit {
 | 
			
		||||
    this.seoService.setTitle($localize`:@@004b222ff9ef9dd4771b777950ca1d0e4cd4348a:About`);
 | 
			
		||||
    this.websocketService.want(['blocks']);
 | 
			
		||||
 | 
			
		||||
    this.sponsors$ = this.apiService.getDonation$()
 | 
			
		||||
      .pipe(
 | 
			
		||||
        tap(() => this.goToAnchor())
 | 
			
		||||
      );
 | 
			
		||||
    this.profiles$ = this.apiService.getAboutPageProfiles$().pipe(
 | 
			
		||||
      tap(() => {
 | 
			
		||||
        this.goToAnchor()
 | 
			
		||||
      }),
 | 
			
		||||
      share(),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    this.translators$ = this.apiService.getTranslators$()
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map((translators) => {
 | 
			
		||||
@ -59,6 +64,9 @@ export class AboutComponent implements OnInit {
 | 
			
		||||
        }),
 | 
			
		||||
        tap(() => this.goToAnchor())
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    this.ogs$ = this.apiService.getOgs$();
 | 
			
		||||
 | 
			
		||||
    this.allContributors$ = this.apiService.getContributor$().pipe(
 | 
			
		||||
      map((contributors) => {
 | 
			
		||||
        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) {
 | 
			
		||||
      this.scene.update(add, remove, change, direction, resetLayout);
 | 
			
		||||
      this.start();
 | 
			
		||||
 | 
			
		||||
@ -150,7 +150,7 @@ export default class BlockScene {
 | 
			
		||||
    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 removed = this.removeBatch(remove, startTime, direction);
 | 
			
		||||
 | 
			
		||||
@ -175,6 +175,7 @@ export default class BlockScene {
 | 
			
		||||
      // update effective rates
 | 
			
		||||
      change.forEach(tx => {
 | 
			
		||||
        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].rate = tx.rate;
 | 
			
		||||
          this.txs[tx.txid].dirty = true;
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ const auditColors = {
 | 
			
		||||
  missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
 | 
			
		||||
  added: hexToColor('0099ff'),
 | 
			
		||||
  selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7),
 | 
			
		||||
  accelerated: hexToColor('8F5FF6'),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// convert from this class's update format to TxSprite's update format
 | 
			
		||||
@ -37,8 +38,9 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
  vsize: number;
 | 
			
		||||
  value: number;
 | 
			
		||||
  feerate: number;
 | 
			
		||||
  acc?: boolean;
 | 
			
		||||
  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';
 | 
			
		||||
  scene?: BlockScene;
 | 
			
		||||
 | 
			
		||||
@ -63,6 +65,7 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
    this.vsize = tx.vsize;
 | 
			
		||||
    this.value = tx.value;
 | 
			
		||||
    this.feerate = tx.rate || (tx.fee / tx.vsize); // sort by effective fee rate where available
 | 
			
		||||
    this.acc = tx.acc;
 | 
			
		||||
    this.rate = tx.rate;
 | 
			
		||||
    this.status = tx.status;
 | 
			
		||||
    this.initialised = false;
 | 
			
		||||
@ -199,6 +202,11 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
    const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1];
 | 
			
		||||
    // Normal mode
 | 
			
		||||
    if (!this.scene?.highlightingEnabled) {
 | 
			
		||||
      if (this.acc) {
 | 
			
		||||
        return auditColors.accelerated;
 | 
			
		||||
      } else {
 | 
			
		||||
        return feeLevelColor;
 | 
			
		||||
      }
 | 
			
		||||
      return feeLevelColor;
 | 
			
		||||
    }
 | 
			
		||||
    // Block audit
 | 
			
		||||
@ -216,6 +224,8 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
        return auditColors.added;
 | 
			
		||||
      case 'selected':
 | 
			
		||||
        return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1];
 | 
			
		||||
      case 'accelerated':
 | 
			
		||||
        return auditColors.accelerated;
 | 
			
		||||
      case 'found':
 | 
			
		||||
        if (this.context === 'projected') {
 | 
			
		||||
          return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1];
 | 
			
		||||
@ -223,7 +233,11 @@ export default class TxView implements TransactionStripped {
 | 
			
		||||
          return feeLevelColor;
 | 
			
		||||
        }
 | 
			
		||||
      default:
 | 
			
		||||
        return feeLevelColor;
 | 
			
		||||
        if (this.acc) {
 | 
			
		||||
          return auditColors.accelerated;
 | 
			
		||||
        } else {
 | 
			
		||||
          return feeLevelColor;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,8 @@
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <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>
 | 
			
		||||
          <app-fee-rate [fee]="effectiveRate"></app-fee-rate>
 | 
			
		||||
        </td>
 | 
			
		||||
@ -54,6 +55,7 @@
 | 
			
		||||
          <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="'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>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </tbody>
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
 | 
			
		||||
  vsize = 1;
 | 
			
		||||
  feeRate = 0;
 | 
			
		||||
  effectiveRate;
 | 
			
		||||
  acceleration;
 | 
			
		||||
 | 
			
		||||
  tooltipPosition: Position = { x: 0, y: 0 };
 | 
			
		||||
 | 
			
		||||
@ -53,6 +54,7 @@ export class BlockOverviewTooltipComponent implements OnChanges {
 | 
			
		||||
      this.vsize = tx.vsize || 1;
 | 
			
		||||
      this.feeRate = this.fee / this.vsize;
 | 
			
		||||
      this.effectiveRate = tx.rate;
 | 
			
		||||
      this.acceleration = tx.acc;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -340,12 +340,16 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
        const isFresh = {};
 | 
			
		||||
        const isSigop = {};
 | 
			
		||||
        const isRbf = {};
 | 
			
		||||
        const isAccelerated = {};
 | 
			
		||||
        this.numMissing = 0;
 | 
			
		||||
        this.numUnexpected = 0;
 | 
			
		||||
 | 
			
		||||
        if (blockAudit?.template) {
 | 
			
		||||
          for (const tx of blockAudit.template) {
 | 
			
		||||
            inTemplate[tx.txid] = true;
 | 
			
		||||
            if (tx.acc) {
 | 
			
		||||
              isAccelerated[tx.txid] = true;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          for (const tx of transactions) {
 | 
			
		||||
            inBlock[tx.txid] = true;
 | 
			
		||||
@ -365,6 +369,9 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
          for (const txid of blockAudit.fullrbfTxs || []) {
 | 
			
		||||
            isRbf[txid] = true;
 | 
			
		||||
          }
 | 
			
		||||
          for (const txid of blockAudit.acceleratedTxs || []) {
 | 
			
		||||
            isAccelerated[txid] = true;
 | 
			
		||||
          }
 | 
			
		||||
          // set transaction statuses
 | 
			
		||||
          for (const tx of blockAudit.template) {
 | 
			
		||||
            tx.context = 'projected';
 | 
			
		||||
@ -389,6 +396,9 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
              isMissing[tx.txid] = true;
 | 
			
		||||
              this.numMissing++;
 | 
			
		||||
            }
 | 
			
		||||
            if (isAccelerated[tx.txid]) {
 | 
			
		||||
              tx.status = 'accelerated';
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          for (const [index, tx] of transactions.entries()) {
 | 
			
		||||
            tx.context = 'actual';
 | 
			
		||||
@ -405,6 +415,9 @@ export class BlockComponent implements OnInit, OnDestroy {
 | 
			
		||||
              isSelected[tx.txid] = true;
 | 
			
		||||
              this.numUnexpected++;
 | 
			
		||||
            }
 | 
			
		||||
            if (isAccelerated[tx.txid]) {
 | 
			
		||||
              tx.status = 'accelerated';
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          for (const tx of transactions) {
 | 
			
		||||
            inBlock[tx.txid] = true;
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,7 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    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 sampleInterval = maxBlockVSize / this.numSamples;
 | 
			
		||||
    let cumVSize = 0;
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
<ng-container *ngIf="{ val: network$ | async } as network">
 | 
			
		||||
<header>
 | 
			
		||||
<header *ngIf="headerVisible">
 | 
			
		||||
  <nav class="navbar navbar-expand-md navbar-dark bg-dark">
 | 
			
		||||
  <a class="navbar-brand" [ngClass]="{'dual-logos': subdomain}" [routerLink]="['/' | relativeUrl]" (click)="brandClick($event)">
 | 
			
		||||
  <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 { Observable, merge, of } from 'rxjs';
 | 
			
		||||
import { LanguageService } from '../../services/language.service';
 | 
			
		||||
@ -11,6 +11,9 @@ import { NavigationService } from '../../services/navigation.service';
 | 
			
		||||
  styleUrls: ['./master-page.component.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class MasterPageComponent implements OnInit {
 | 
			
		||||
  @Input() headerVisible = true;
 | 
			
		||||
  @Input() footerVisibleOverride: boolean | null = null;
 | 
			
		||||
 | 
			
		||||
  env: Env;
 | 
			
		||||
  network$: Observable<string>;
 | 
			
		||||
  connectionState$: Observable<number>;
 | 
			
		||||
@ -38,10 +41,14 @@ export class MasterPageComponent implements OnInit {
 | 
			
		||||
    this.subdomain = this.enterpriseService.getSubdomain();
 | 
			
		||||
    this.navigationService.subnetPaths.subscribe((paths) => {
 | 
			
		||||
      this.networkPaths = paths;
 | 
			
		||||
      if (paths.mainnet.indexOf('docs') > -1) {
 | 
			
		||||
        this.footerVisible = false;
 | 
			
		||||
      if (this.footerVisibleOverride === null) {
 | 
			
		||||
        if (paths.mainnet.indexOf('docs') > -1) {
 | 
			
		||||
          this.footerVisible = false;
 | 
			
		||||
        } else {
 | 
			
		||||
          this.footerVisible = true;
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        this.footerVisible = true;
 | 
			
		||||
        this.footerVisible = this.footerVisibleOverride;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -94,7 +94,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
 | 
			
		||||
 | 
			
		||||
  updateBlock(delta: MempoolBlockDelta): void {
 | 
			
		||||
    const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
 | 
			
		||||
 | 
			
		||||
    if (this.blockIndex !== this.index) {
 | 
			
		||||
      const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection;
 | 
			
		||||
      this.blockGraph.replace(delta.added, direction);
 | 
			
		||||
 | 
			
		||||
@ -49,7 +49,7 @@
 | 
			
		||||
        </div>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </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>
 | 
			
		||||
</ng-container>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -169,4 +169,34 @@
 | 
			
		||||
  transform: translate(calc(-0.2 * var(--block-size)), calc(1.1 * var(--block-size)));
 | 
			
		||||
  border-radius: 2px;
 | 
			
		||||
  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 {
 | 
			
		||||
  @Input() minimal: boolean = false;
 | 
			
		||||
  @Input() blockWidth: number = 125;
 | 
			
		||||
  @Input() containerWidth: number = null;
 | 
			
		||||
  @Input() count: number = null;
 | 
			
		||||
  @Input() spotlight: number = 0;
 | 
			
		||||
  @Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`;
 | 
			
		||||
 | 
			
		||||
@ -99,14 +99,20 @@
 | 
			
		||||
                    </ng-template>
 | 
			
		||||
                    <ng-template #estimationTmpl>
 | 
			
		||||
                      <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 #belowBlockLimit>
 | 
			
		||||
                        <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>
 | 
			
		||||
                        </ng-template>
 | 
			
		||||
                        <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>
 | 
			
		||||
@ -488,7 +494,8 @@
 | 
			
		||||
        </td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <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>
 | 
			
		||||
          <div class="effective-fee-container">
 | 
			
		||||
            <app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
 | 
			
		||||
 | 
			
		||||
@ -216,4 +216,23 @@
 | 
			
		||||
  .alert-link {
 | 
			
		||||
    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 relativeUrlPipe: RelativeUrlPipe,
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private cacheService: CacheService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private audioService: AudioService,
 | 
			
		||||
@ -183,6 +183,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
 | 
			
		||||
        } else {
 | 
			
		||||
          this.tx.effectiveFeePerVsize = cpfpInfo.effectiveFeePerVsize;
 | 
			
		||||
        }
 | 
			
		||||
        if (cpfpInfo.acceleration) {
 | 
			
		||||
          this.tx.acceleration = cpfpInfo.acceleration;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.cpfpInfo = cpfpInfo;
 | 
			
		||||
        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[];
 | 
			
		||||
  bestDescendant?: BestDescendant | null;
 | 
			
		||||
  cpfpChecked?: boolean;
 | 
			
		||||
  acceleration?: number;
 | 
			
		||||
  deleteAfter?: number;
 | 
			
		||||
  _unblinded?: any;
 | 
			
		||||
  _deduced?: boolean;
 | 
			
		||||
 | 
			
		||||
@ -27,6 +27,7 @@ export interface CpfpInfo {
 | 
			
		||||
  effectiveFeePerVsize?: number;
 | 
			
		||||
  sigops?: number;
 | 
			
		||||
  adjustedVsize?: number;
 | 
			
		||||
  acceleration?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RbfInfo {
 | 
			
		||||
@ -111,6 +112,7 @@ export interface PoolInfo {
 | 
			
		||||
  addresses: string; // JSON array
 | 
			
		||||
  emptyBlocks: number;
 | 
			
		||||
  slug: string;
 | 
			
		||||
  poolUniqueId: number;
 | 
			
		||||
}
 | 
			
		||||
export interface PoolStat {
 | 
			
		||||
  pool: PoolInfo;
 | 
			
		||||
@ -159,6 +161,7 @@ export interface BlockAudit extends BlockExtended {
 | 
			
		||||
  freshTxs: string[],
 | 
			
		||||
  sigopTxs: string[],
 | 
			
		||||
  fullrbfTxs: string[],
 | 
			
		||||
  acceleratedTxs: string[],
 | 
			
		||||
  matchRate: number,
 | 
			
		||||
  expectedFees: number,
 | 
			
		||||
  expectedWeight: number,
 | 
			
		||||
@ -175,7 +178,8 @@ export interface TransactionStripped {
 | 
			
		||||
  vsize: number;
 | 
			
		||||
  value: number;
 | 
			
		||||
  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';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -187,6 +191,7 @@ export interface RbfTransaction extends TransactionStripped {
 | 
			
		||||
export interface MempoolPosition {
 | 
			
		||||
  block: number,
 | 
			
		||||
  vsize: number,
 | 
			
		||||
  accelerated?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RewardStats {
 | 
			
		||||
 | 
			
		||||
@ -70,7 +70,7 @@ export interface MempoolBlockWithTransactions extends MempoolBlock {
 | 
			
		||||
export interface MempoolBlockDelta {
 | 
			
		||||
  added: TransactionStripped[],
 | 
			
		||||
  removed: string[],
 | 
			
		||||
  changed?: { txid: string, rate: number | undefined }[];
 | 
			
		||||
  changed?: { txid: string, rate: number | undefined, acc: boolean | undefined }[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolInfo {
 | 
			
		||||
@ -88,8 +88,9 @@ export interface TransactionStripped {
 | 
			
		||||
  fee: number;
 | 
			
		||||
  vsize: number;
 | 
			
		||||
  value: number;
 | 
			
		||||
  acc?: boolean; // is accelerated?
 | 
			
		||||
  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';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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 { NodeChannels } from '../lightning/nodes-channels/node-channels.component';
 | 
			
		||||
import { GroupComponent } from './group/group.component';
 | 
			
		||||
import { NodeOwnerComponent } from './node-owner/node-owner.component';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
@ -66,6 +67,7 @@ import { GroupComponent } from './group/group.component';
 | 
			
		||||
    NodesRankingsDashboard,
 | 
			
		||||
    NodeChannels,
 | 
			
		||||
    GroupComponent,
 | 
			
		||||
    NodeOwnerComponent,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonModule,
 | 
			
		||||
@ -103,6 +105,7 @@ import { GroupComponent } from './group/group.component';
 | 
			
		||||
    OldestNodes,
 | 
			
		||||
    NodesRankingsDashboard,
 | 
			
		||||
    NodeChannels,
 | 
			
		||||
    NodeOwnerComponent,
 | 
			
		||||
  ],
 | 
			
		||||
  providers: [
 | 
			
		||||
    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">
 | 
			
		||||
    <h5 class="mb-0" style="color: #ffffff66" i18n="lightning.node">Lightning node</h5>
 | 
			
		||||
    <div class="title-container mb-2">
 | 
			
		||||
      <h1 class="mb-0 text-truncate">{{ node.alias }}</h1>
 | 
			
		||||
      <span class="tx-link">
 | 
			
		||||
      <div class="d-flex justify-content-between align-items-center">
 | 
			
		||||
        <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">
 | 
			
		||||
          <app-truncate [text]="node.public_key" [lastChars]="8" [link]="['/lightning/node' | relativeUrl, node.public_key]">
 | 
			
		||||
            <app-clipboard [text]="node.public_key"></app-clipboard>
 | 
			
		||||
          </app-truncate>
 | 
			
		||||
        </span>
 | 
			
		||||
        <!-- <app-node-owner [nodeOwner$]="nodeOwner$" [publicKey]="node.public_key" [alias]="node.alias" class="claim-btn-mobile"></app-node-owner> -->
 | 
			
		||||
      </span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
 | 
			
		||||
@ -111,3 +111,17 @@ app-fiat {
 | 
			
		||||
  margin: 0 0.25em;
 | 
			
		||||
  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 { Observable } from 'rxjs';
 | 
			
		||||
import { catchError, map, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { Observable, of, EMPTY } from 'rxjs';
 | 
			
		||||
import { catchError, map, switchMap, tap, share } from 'rxjs/operators';
 | 
			
		||||
import { SeoService } from '../../services/seo.service';
 | 
			
		||||
import { ApiService } from '../../services/api.service';
 | 
			
		||||
import { LightningApiService } from '../lightning-api.service';
 | 
			
		||||
@ -38,6 +38,7 @@ export class NodeComponent implements OnInit {
 | 
			
		||||
  tlvRecords: CustomRecord[];
 | 
			
		||||
  avgChannelDistance$: Observable<number | null>;
 | 
			
		||||
  showFeatures = false;
 | 
			
		||||
  nodeOwner$: Observable<any>;
 | 
			
		||||
  kmToMiles = kmToMiles;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
@ -45,6 +46,7 @@ export class NodeComponent implements OnInit {
 | 
			
		||||
    private lightningApiService: LightningApiService,
 | 
			
		||||
    private activatedRoute: ActivatedRoute,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private cd: ChangeDetectorRef,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
@ -147,6 +149,24 @@ export class NodeComponent implements OnInit {
 | 
			
		||||
        return 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 {
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,8 @@ import { WebsocketResponse } from '../interfaces/websocket.interface';
 | 
			
		||||
import { Outspend, Transaction } from '../interfaces/electrs.interface';
 | 
			
		||||
import { Conversion } from './price.service';
 | 
			
		||||
 | 
			
		||||
const SERVICES_API_PREFIX = `/api/v1/services`;
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root'
 | 
			
		||||
})
 | 
			
		||||
@ -92,15 +94,11 @@ export class ApiService {
 | 
			
		||||
    return this.httpClient.get<Outspend[][]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/outspends', { params });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  requestDonation$(amount: number, orderId: string): Observable<any> {
 | 
			
		||||
    const params = {
 | 
			
		||||
      amount: amount,
 | 
			
		||||
      orderId: orderId,
 | 
			
		||||
    };
 | 
			
		||||
    return this.httpClient.post<any>(this.apiBaseUrl + '/api/v1/donations', params);
 | 
			
		||||
  getAboutPageProfiles$(): Observable<any[]> {
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/about-page');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getDonation$(): Observable<any[]> {
 | 
			
		||||
  getOgs$(): Observable<any> {
 | 
			
		||||
    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');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  checkDonation$(orderId: string): Observable<any[]> {
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + '/api/v1/donations/check?order_id=' + orderId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getInitData$(): Observable<WebsocketResponse> {
 | 
			
		||||
    return this.httpClient.get<WebsocketResponse>(this.apiBaseUrl + this.apiBasePath + '/api/v1/init-data');
 | 
			
		||||
  }
 | 
			
		||||
@ -323,4 +317,13 @@ export class ApiService {
 | 
			
		||||
        (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;
 | 
			
		||||
  SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
 | 
			
		||||
  HISTORICAL_PRICE: boolean;
 | 
			
		||||
  ACCELERATOR: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultEnv: Env = {
 | 
			
		||||
@ -77,6 +78,7 @@ const defaultEnv: Env = {
 | 
			
		||||
  'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
 | 
			
		||||
  'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
 | 
			
		||||
  'HISTORICAL_PRICE': true,
 | 
			
		||||
  'ACCELERATOR': false,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
<footer>
 | 
			
		||||
    <div class="container-fluid">
 | 
			
		||||
    <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">
 | 
			
		||||
          <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>
 | 
			
		||||
@ -16,10 +16,12 @@
 | 
			
		||||
        <div class="selector">
 | 
			
		||||
          <app-rate-unit-selector></app-rate-unit-selector>
 | 
			
		||||
        </div>
 | 
			
		||||
        <ng-template #temporaryHidden>
 | 
			
		||||
          <a *ngIf="officialMempoolSpace" class="cta btn btn-purple sponsor" [routerLink]="['/signup' | relativeUrl]">Support the Project</a>
 | 
			
		||||
          <p *ngIf="officialMempoolSpace && env.BASE_MODULE === 'mempool'" class="cta-secondary"><a [routerLink]="['/signin' | relativeUrl]" i18n="shared.broadcast-transaction|Broadcast Transaction">Sign In</a></p>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
        <div *ngIf="officialMempoolSpace && stateService.env.ACCELERATOR" class="cta">
 | 
			
		||||
          <a class="btn btn-purple sponsor" [routerLink]="['/login' | relativeUrl]">
 | 
			
		||||
            <span *ngIf="loggedIn" i18n="shared.my-account">My Account</span>
 | 
			
		||||
            <span *ngIf="!loggedIn" i18n="shared.sign-in">Sign In / Sign Up</span>
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="col-lg-6 col-md-10 offset-md-1 links outer">
 | 
			
		||||
        <div class="row">
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@ footer .row.main .branding {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer .row.main .branding > p {
 | 
			
		||||
  margin-bottom: 45px;
 | 
			
		||||
  margin-bottom: 25px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer .row.main .branding .btn {
 | 
			
		||||
@ -35,11 +35,7 @@ footer .row.main .branding button.account {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer .row.main .branding .cta {
 | 
			
		||||
  margin: 20px auto 25px auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer .row.main .branding .cta-secondary {
 | 
			
		||||
 | 
			
		||||
  margin: 25px auto 25px auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
footer .row.main .links.outer {
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,13 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit, Inject, LOCALE_ID } from '@angular/core';
 | 
			
		||||
import { Observable, merge, of, Subject } from 'rxjs';
 | 
			
		||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, Inject, LOCALE_ID } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
import { Observable, merge, of, Subject, Subscription } from 'rxjs';
 | 
			
		||||
import { tap, takeUntil } from 'rxjs/operators';
 | 
			
		||||
import { Env, StateService } from '../../../services/state.service';
 | 
			
		||||
import { IBackendInfo } from '../../../interfaces/websocket.interface';
 | 
			
		||||
import { LanguageService } from '../../../services/language.service';
 | 
			
		||||
import { NavigationService } from '../../../services/navigation.service';
 | 
			
		||||
import { StorageService } from '../../../services/storage.service';
 | 
			
		||||
import { WebsocketService } from '../../../services/websocket.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-global-footer',
 | 
			
		||||
@ -23,12 +26,19 @@ export class GlobalFooterComponent implements OnInit {
 | 
			
		||||
  network$: Observable<string>;
 | 
			
		||||
  networkPaths: { [network: string]: string };
 | 
			
		||||
  currentNetwork = '';
 | 
			
		||||
  loggedIn = false;
 | 
			
		||||
  username = null;
 | 
			
		||||
  urlSubscription: Subscription;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public stateService: StateService,
 | 
			
		||||
    private languageService: LanguageService,
 | 
			
		||||
    private navigationService: NavigationService,
 | 
			
		||||
    @Inject(LOCALE_ID) public locale: string,
 | 
			
		||||
    private storageService: StorageService,
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    private cd: ChangeDetectorRef,
 | 
			
		||||
    private websocketService: WebsocketService
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
@ -46,11 +56,23 @@ export class GlobalFooterComponent implements OnInit {
 | 
			
		||||
    this.network$.pipe(takeUntil(this.destroy$)).subscribe((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 {
 | 
			
		||||
    this.destroy$.next(true);
 | 
			
		||||
    this.destroy$.complete();
 | 
			
		||||
    this.urlSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  networkLink(network) {
 | 
			
		||||
 | 
			
		||||
@ -219,6 +219,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
 | 
			
		||||
    AmountShortenerPipe,
 | 
			
		||||
  ],
 | 
			
		||||
  exports: [
 | 
			
		||||
    MasterPageComponent,
 | 
			
		||||
    RouterModule,
 | 
			
		||||
    ReactiveFormsModule,
 | 
			
		||||
    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