mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 06:39:58 -07:00
global: snapshot
This commit is contained in:
@@ -127,9 +127,17 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
|||||||
writeln!(output, " if (format === 'csv') {{").unwrap();
|
writeln!(output, " if (format === 'csv') {{").unwrap();
|
||||||
writeln!(output, " return this.getText(path, {{ signal }});").unwrap();
|
writeln!(output, " return this.getText(path, {{ signal }});").unwrap();
|
||||||
writeln!(output, " }}").unwrap();
|
writeln!(output, " }}").unwrap();
|
||||||
writeln!(output, " return this.getJson(path, {{ signal, onUpdate }});").unwrap();
|
writeln!(
|
||||||
|
output,
|
||||||
|
" return this.getJson(path, {{ signal, onUpdate }});"
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
} else {
|
} else {
|
||||||
writeln!(output, " return this.getJson(path, {{ signal, onUpdate }});").unwrap();
|
writeln!(
|
||||||
|
output,
|
||||||
|
" return this.getJson(path, {{ signal, onUpdate }});"
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -401,6 +401,4 @@ impl Stores {
|
|||||||
.remove(AddrIndexTxIndex::from((addr_index, tx_index)));
|
.remove(AddrIndexTxIndex::from((addr_index, tx_index)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -252,7 +252,6 @@ impl Query {
|
|||||||
let fa_pct90 = fad.pct90.height.collect_range_at(begin, end);
|
let fa_pct90 = fad.pct90.height.collect_range_at(begin, end);
|
||||||
let fa_max = fad.max.height.collect_range_at(begin, end);
|
let fa_max = fad.max.height.collect_range_at(begin, end);
|
||||||
|
|
||||||
|
|
||||||
// Bulk read median time window
|
// Bulk read median time window
|
||||||
let median_start = begin.saturating_sub(10);
|
let median_start = begin.saturating_sub(10);
|
||||||
let median_timestamps = indexer
|
let median_timestamps = indexer
|
||||||
@@ -272,20 +271,47 @@ impl Query {
|
|||||||
|
|
||||||
// Single reader for header + coinbase (adjacent in blk file)
|
// Single reader for header + coinbase (adjacent in blk file)
|
||||||
let varint_len = Self::compact_size_len(tx_count) as usize;
|
let varint_len = Self::compact_size_len(tx_count) as usize;
|
||||||
let (raw_header, coinbase_raw, coinbase_address, coinbase_addresses, coinbase_signature, coinbase_signature_ascii, scriptsig_bytes) = match reader.reader_at(positions[i]) {
|
let (
|
||||||
|
raw_header,
|
||||||
|
coinbase_raw,
|
||||||
|
coinbase_address,
|
||||||
|
coinbase_addresses,
|
||||||
|
coinbase_signature,
|
||||||
|
coinbase_signature_ascii,
|
||||||
|
scriptsig_bytes,
|
||||||
|
) = match reader.reader_at(positions[i]) {
|
||||||
Ok(mut blk) => {
|
Ok(mut blk) => {
|
||||||
let mut header_buf = [0u8; HEADER_SIZE];
|
let mut header_buf = [0u8; HEADER_SIZE];
|
||||||
if blk.read_exact(&mut header_buf).is_err() {
|
if blk.read_exact(&mut header_buf).is_err() {
|
||||||
([0u8; HEADER_SIZE], String::new(), None, vec![], String::new(), String::new(), vec![])
|
(
|
||||||
|
[0u8; HEADER_SIZE],
|
||||||
|
String::new(),
|
||||||
|
None,
|
||||||
|
vec![],
|
||||||
|
String::new(),
|
||||||
|
String::new(),
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// Skip tx count varint
|
// Skip tx count varint
|
||||||
let mut skip = [0u8; 5];
|
let mut skip = [0u8; 5];
|
||||||
let _ = blk.read_exact(&mut skip[..varint_len]);
|
let _ = blk.read_exact(&mut skip[..varint_len]);
|
||||||
let coinbase = Self::parse_coinbase_from_read(blk);
|
let coinbase = Self::parse_coinbase_from_read(blk);
|
||||||
(header_buf, coinbase.0, coinbase.1, coinbase.2, coinbase.3, coinbase.4, coinbase.5)
|
(
|
||||||
|
header_buf, coinbase.0, coinbase.1, coinbase.2, coinbase.3, coinbase.4,
|
||||||
|
coinbase.5,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => ([0u8; HEADER_SIZE], String::new(), None, vec![], String::new(), String::new(), vec![]),
|
Err(_) => (
|
||||||
|
[0u8; HEADER_SIZE],
|
||||||
|
String::new(),
|
||||||
|
None,
|
||||||
|
vec![],
|
||||||
|
String::new(),
|
||||||
|
String::new(),
|
||||||
|
vec![],
|
||||||
|
),
|
||||||
};
|
};
|
||||||
let header = Self::decode_header(&raw_header)?;
|
let header = Self::decode_header(&raw_header)?;
|
||||||
|
|
||||||
@@ -517,12 +543,20 @@ impl Query {
|
|||||||
fn parse_coinbase_from_read(
|
fn parse_coinbase_from_read(
|
||||||
reader: impl Read,
|
reader: impl Read,
|
||||||
) -> (String, Option<String>, Vec<String>, String, String, Vec<u8>) {
|
) -> (String, Option<String>, Vec<String>, String, String, Vec<u8>) {
|
||||||
let empty = (String::new(), None, vec![], String::new(), String::new(), vec![]);
|
let empty = (
|
||||||
|
String::new(),
|
||||||
|
None,
|
||||||
|
vec![],
|
||||||
|
String::new(),
|
||||||
|
String::new(),
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
|
||||||
let tx = match bitcoin::Transaction::consensus_decode(&mut bitcoin::io::FromStd::new(reader)) {
|
let tx =
|
||||||
Ok(tx) => tx,
|
match bitcoin::Transaction::consensus_decode(&mut bitcoin::io::FromStd::new(reader)) {
|
||||||
Err(_) => return empty,
|
Ok(tx) => tx,
|
||||||
};
|
Err(_) => return empty,
|
||||||
|
};
|
||||||
|
|
||||||
let scriptsig_bytes: Vec<u8> = tx
|
let scriptsig_bytes: Vec<u8> = tx
|
||||||
.input
|
.input
|
||||||
@@ -532,10 +566,7 @@ impl Query {
|
|||||||
|
|
||||||
let coinbase_raw = scriptsig_bytes.to_lower_hex_string();
|
let coinbase_raw = scriptsig_bytes.to_lower_hex_string();
|
||||||
|
|
||||||
let coinbase_signature_ascii: String = scriptsig_bytes
|
let coinbase_signature_ascii: String = scriptsig_bytes.iter().map(|&b| b as char).collect();
|
||||||
.iter()
|
|
||||||
.map(|&b| b as char)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut coinbase_addresses: Vec<String> = tx
|
let mut coinbase_addresses: Vec<String> = tx
|
||||||
.output
|
.output
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ impl Query {
|
|||||||
pub fn block_fee_rates(&self, time_period: TimePeriod) -> Result<Vec<BlockFeeRatesEntry>> {
|
pub fn block_fee_rates(&self, time_period: TimePeriod) -> Result<Vec<BlockFeeRatesEntry>> {
|
||||||
let bw = BlockWindow::new(self, time_period);
|
let bw = BlockWindow::new(self, time_period);
|
||||||
let computer = self.computer();
|
let computer = self.computer();
|
||||||
let frd = &computer.transactions.fees.effective_fee_rate.distribution.block;
|
let frd = &computer
|
||||||
|
.transactions
|
||||||
|
.fees
|
||||||
|
.effective_fee_rate
|
||||||
|
.distribution
|
||||||
|
.block;
|
||||||
|
|
||||||
let min = frd.min.height.collect_range_at(bw.start, bw.end);
|
let min = frd.min.height.collect_range_at(bw.start, bw.end);
|
||||||
let pct10 = frd.pct10.height.collect_range_at(bw.start, bw.end);
|
let pct10 = frd.pct10.height.collect_range_at(bw.start, bw.end);
|
||||||
|
|||||||
@@ -62,10 +62,7 @@ impl Query {
|
|||||||
let mut hashrates = Vec::with_capacity(total_days / step + 1);
|
let mut hashrates = Vec::with_capacity(total_days / step + 1);
|
||||||
let mut di = start_day1.to_usize();
|
let mut di = start_day1.to_usize();
|
||||||
while di <= end_day1.to_usize() {
|
while di <= end_day1.to_usize() {
|
||||||
if let (Some(Some(hr)), Some(timestamp)) = (
|
if let (Some(Some(hr)), Some(timestamp)) = (hr_cursor.get(di), ts_cursor.get(di)) {
|
||||||
hr_cursor.get(di),
|
|
||||||
ts_cursor.get(di),
|
|
||||||
) {
|
|
||||||
hashrates.push(HashrateEntry {
|
hashrates.push(HashrateEntry {
|
||||||
timestamp,
|
timestamp,
|
||||||
avg_hashrate: *hr as u128,
|
avg_hashrate: *hr as u128,
|
||||||
|
|||||||
@@ -53,8 +53,18 @@ impl Query {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get block info for status
|
// Get block info for status
|
||||||
let height = indexer.vecs.transactions.height.collect_one(tx_index).unwrap();
|
let height = indexer
|
||||||
let block_hash = indexer.vecs.blocks.blockhash.reader().get(height.to_usize());
|
.vecs
|
||||||
|
.transactions
|
||||||
|
.height
|
||||||
|
.collect_one(tx_index)
|
||||||
|
.unwrap();
|
||||||
|
let block_hash = indexer
|
||||||
|
.vecs
|
||||||
|
.blocks
|
||||||
|
.blockhash
|
||||||
|
.reader()
|
||||||
|
.get(height.to_usize());
|
||||||
let block_time = indexer.vecs.blocks.timestamp.collect_one(height).unwrap();
|
let block_time = indexer.vecs.blocks.timestamp.collect_one(height).unwrap();
|
||||||
|
|
||||||
Ok(TxStatus {
|
Ok(TxStatus {
|
||||||
@@ -110,7 +120,10 @@ impl Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn outspend(&self, txid: &Txid, vout: Vout) -> Result<TxOutspend> {
|
pub fn outspend(&self, txid: &Txid, vout: Vout) -> Result<TxOutspend> {
|
||||||
if self.mempool().is_some_and(|m| m.get_txs().contains_key(txid)) {
|
if self
|
||||||
|
.mempool()
|
||||||
|
.is_some_and(|m| m.get_txs().contains_key(txid))
|
||||||
|
{
|
||||||
return Ok(TxOutspend::UNSPENT);
|
return Ok(TxOutspend::UNSPENT);
|
||||||
}
|
}
|
||||||
let (_, first_txout, output_count) = self.resolve_tx_outputs(txid)?;
|
let (_, first_txout, output_count) = self.resolve_tx_outputs(txid)?;
|
||||||
@@ -150,8 +163,7 @@ impl Query {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let spending_tx_index = input_tx_cursor.get(usize::from(txin_index)).unwrap();
|
let spending_tx_index = input_tx_cursor.get(usize::from(txin_index)).unwrap();
|
||||||
let spending_first_txin =
|
let spending_first_txin = first_txin_cursor.get(spending_tx_index.to_usize()).unwrap();
|
||||||
first_txin_cursor.get(spending_tx_index.to_usize()).unwrap();
|
|
||||||
let vin = Vin::from(usize::from(txin_index) - usize::from(spending_first_txin));
|
let vin = Vin::from(usize::from(txin_index) - usize::from(spending_first_txin));
|
||||||
let spending_txid = txid_reader.get(spending_tx_index.to_usize());
|
let spending_txid = txid_reader.get(spending_tx_index.to_usize());
|
||||||
let spending_height = height_cursor.get(spending_tx_index.to_usize()).unwrap();
|
let spending_height = height_cursor.get(spending_tx_index.to_usize()).unwrap();
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ use std::{
|
|||||||
any::Any,
|
any::Any,
|
||||||
net::SocketAddr,
|
net::SocketAddr,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{
|
sync::{Arc, atomic::AtomicU64},
|
||||||
Arc,
|
|
||||||
atomic::AtomicU64,
|
|
||||||
},
|
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -377,5 +377,4 @@ where
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ pub struct CpfpInfo {
|
|||||||
pub adjusted_vsize: Option<VSize>,
|
pub adjusted_vsize: Option<VSize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// A transaction in a CPFP relationship
|
/// A transaction in a CPFP relationship
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct CpfpEntry {
|
pub struct CpfpEntry {
|
||||||
|
|||||||
@@ -17,7 +17,13 @@ use super::{Bitcoin, CentsSigned, Close, High, Sats, StoredF32, StoredF64};
|
|||||||
|
|
||||||
/// US Dollar amount
|
/// US Dollar amount
|
||||||
#[derive(Debug, Default, Clone, Copy, Deref, Serialize, Deserialize, Pco, JsonSchema)]
|
#[derive(Debug, Default, Clone, Copy, Deref, Serialize, Deserialize, Pco, JsonSchema)]
|
||||||
#[schemars(example = 0.0, example = 100.50, example = 30_000.0, example = 69_000.0, example = 84_342.12)]
|
#[schemars(
|
||||||
|
example = &0.0,
|
||||||
|
example = &100.50,
|
||||||
|
example = &30_000.0,
|
||||||
|
example = &69_000.0,
|
||||||
|
example = &84_342.12
|
||||||
|
)]
|
||||||
pub struct Dollars(f64);
|
pub struct Dollars(f64);
|
||||||
|
|
||||||
impl Hash for Dollars {
|
impl Hash for Dollars {
|
||||||
|
|||||||
@@ -11,7 +11,14 @@ use super::{Sats, VSize, Weight};
|
|||||||
|
|
||||||
/// Fee rate in sat/vB
|
/// Fee rate in sat/vB
|
||||||
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Pco, JsonSchema)]
|
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Pco, JsonSchema)]
|
||||||
#[schemars(example = 1.0, example = 2.5, example = 10.14, example = 25.0, example = 302.11)]
|
#[schemars(
|
||||||
|
example = &0.1,
|
||||||
|
example = &1.0,
|
||||||
|
example = &2.5,
|
||||||
|
example = &10.14,
|
||||||
|
example = &25.0,
|
||||||
|
example = &302.11
|
||||||
|
)]
|
||||||
pub struct FeeRate(f64);
|
pub struct FeeRate(f64);
|
||||||
|
|
||||||
impl FeeRate {
|
impl FeeRate {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use vecdb::{Formattable, Pco};
|
|||||||
|
|
||||||
/// Transaction locktime. Values below 500,000,000 are interpreted as block heights; values at or above are Unix timestamps.
|
/// Transaction locktime. Values below 500,000,000 are interpreted as block heights; values at or above are Unix timestamps.
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Pco, JsonSchema)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Pco, JsonSchema)]
|
||||||
#[schemars(example = 0, example = 840000, example = 840001, example = 1713571200, example = 4294967295_u32)]
|
#[schemars(example = &0, example = &840000, example = &840001, example = &1713571200)]
|
||||||
pub struct RawLockTime(u32);
|
pub struct RawLockTime(u32);
|
||||||
|
|
||||||
impl From<LockTime> for RawLockTime {
|
impl From<LockTime> for RawLockTime {
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ use super::{Bitcoin, Cents, Dollars, Height};
|
|||||||
JsonSchema,
|
JsonSchema,
|
||||||
)]
|
)]
|
||||||
#[schemars(
|
#[schemars(
|
||||||
example = 0,
|
example = &0,
|
||||||
example = 546,
|
example = &546,
|
||||||
example = 10000,
|
example = &10000,
|
||||||
example = 100_000_000,
|
example = &100_000_000,
|
||||||
example = 2_100_000_000_000_000_u64
|
example = &2_100_000_000_000_000_u64
|
||||||
)]
|
)]
|
||||||
pub struct Sats(u64);
|
pub struct Sats(u64);
|
||||||
|
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ use super::Date;
|
|||||||
JsonSchema,
|
JsonSchema,
|
||||||
)]
|
)]
|
||||||
#[schemars(
|
#[schemars(
|
||||||
example = 1231006505,
|
example = &1231006505,
|
||||||
example = 1672531200,
|
example = &1672531200,
|
||||||
example = 1713571200,
|
example = &1713571200,
|
||||||
example = 1743631892,
|
example = &1743631892,
|
||||||
example = 1759000868
|
example = &1759000868
|
||||||
)]
|
)]
|
||||||
pub struct Timestamp(u32);
|
pub struct Timestamp(u32);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
FeeRate, RawLockTime, Sats, TxIn, TxIndex, TxOut, TxStatus, TxVersionRaw, Txid, VSize,
|
FeeRate, RawLockTime, Sats, TxIn, TxIndex, TxOut, TxStatus, TxVersionRaw, Txid, VSize, Weight,
|
||||||
Weight,
|
|
||||||
};
|
};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ use serde::{Deserialize, Serialize};
|
|||||||
/// Unlike TxVersion (u8, indexed), this preserves non-standard values
|
/// Unlike TxVersion (u8, indexed), this preserves non-standard values
|
||||||
/// used in coinbase txs for miner signaling/branding.
|
/// used in coinbase txs for miner signaling/branding.
|
||||||
#[derive(Debug, Deref, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Deref, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||||
#[schemars(example = 1, example = 2, example = 3, example = 536_870_912, example = 805_306_368)]
|
#[schemars(
|
||||||
|
example = &1,
|
||||||
|
example = &2,
|
||||||
|
example = &3,
|
||||||
|
example = &536_870_912,
|
||||||
|
example = &805_306_368
|
||||||
|
)]
|
||||||
pub struct TxVersionRaw(i32);
|
pub struct TxVersionRaw(i32);
|
||||||
|
|
||||||
impl From<bitcoin::transaction::Version> for TxVersionRaw {
|
impl From<bitcoin::transaction::Version> for TxVersionRaw {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
#[derive(
|
#[derive(
|
||||||
Debug, Deref, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
|
Debug, Deref, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
|
||||||
)]
|
)]
|
||||||
#[schemars(example = 0, example = 1, example = 2, example = 5, example = 10)]
|
#[schemars(example = &0, example = &1, example = &2, example = &5, example = &10)]
|
||||||
pub struct Vin(u16);
|
pub struct Vin(u16);
|
||||||
|
|
||||||
impl Vin {
|
impl Vin {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use vecdb::{Bytes, Formattable};
|
|||||||
Bytes,
|
Bytes,
|
||||||
Hash,
|
Hash,
|
||||||
)]
|
)]
|
||||||
#[schemars(example = 0, example = 1, example = 2, example = 5, example = 10)]
|
#[schemars(example = &0, example = &1, example = &2, example = &5, example = &10)]
|
||||||
pub struct Vout(u16);
|
pub struct Vout(u16);
|
||||||
|
|
||||||
impl Vout {
|
impl Vout {
|
||||||
|
|||||||
@@ -23,7 +23,13 @@ use crate::Weight;
|
|||||||
Pco,
|
Pco,
|
||||||
JsonSchema,
|
JsonSchema,
|
||||||
)]
|
)]
|
||||||
#[schemars(example = 110, example = 140, example = 225, example = 500_000, example = 998_368)]
|
#[schemars(
|
||||||
|
example = &110,
|
||||||
|
example = &140,
|
||||||
|
example = &225,
|
||||||
|
example = &500_000,
|
||||||
|
example = &998_368
|
||||||
|
)]
|
||||||
pub struct VSize(u64);
|
pub struct VSize(u64);
|
||||||
|
|
||||||
impl VSize {
|
impl VSize {
|
||||||
|
|||||||
@@ -23,7 +23,13 @@ use crate::VSize;
|
|||||||
Pco,
|
Pco,
|
||||||
JsonSchema,
|
JsonSchema,
|
||||||
)]
|
)]
|
||||||
#[schemars(example = 396, example = 561, example = 900, example = 2_000_000, example = 3_993_472)]
|
#[schemars(
|
||||||
|
example = &396,
|
||||||
|
example = &561,
|
||||||
|
example = &900,
|
||||||
|
example = &2_000_000,
|
||||||
|
example = &3_993_472
|
||||||
|
)]
|
||||||
pub struct Weight(u64);
|
pub struct Weight(u64);
|
||||||
|
|
||||||
impl Weight {
|
impl Weight {
|
||||||
|
|||||||
@@ -6628,7 +6628,7 @@ function createTransferPattern(client, acc) {
|
|||||||
* @extends BrkClientBase
|
* @extends BrkClientBase
|
||||||
*/
|
*/
|
||||||
class BrkClient extends BrkClientBase {
|
class BrkClient extends BrkClientBase {
|
||||||
VERSION = "v0.3.0-alpha.6";
|
VERSION = "v0.3.0-beta.0";
|
||||||
|
|
||||||
INDEXES = /** @type {const} */ ([
|
INDEXES = /** @type {const} */ ([
|
||||||
"minute10",
|
"minute10",
|
||||||
|
|||||||
@@ -40,5 +40,5 @@
|
|||||||
"url": "git+https://github.com/bitcoinresearchkit/brk.git"
|
"url": "git+https://github.com/bitcoinresearchkit/brk.git"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.3.0-alpha.6"
|
"version": "0.3.0-beta.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6079,7 +6079,7 @@ class SeriesTree:
|
|||||||
class BrkClient(BrkClientBase):
|
class BrkClient(BrkClientBase):
|
||||||
"""Main BRK client with series tree and API methods."""
|
"""Main BRK client with series tree and API methods."""
|
||||||
|
|
||||||
VERSION = "v0.3.0-alpha.6"
|
VERSION = "v0.3.0-beta.0"
|
||||||
|
|
||||||
INDEXES = [
|
INDEXES = [
|
||||||
"minute10",
|
"minute10",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "brk-client"
|
name = "brk-client"
|
||||||
version = "0.3.0-alpha.6"
|
version = "0.3.0-beta.0"
|
||||||
description = "Bitcoin on-chain analytics client — thousands of metrics, block explorer, and address index"
|
description = "Bitcoin on-chain analytics client — thousands of metrics, block explorer, and address index"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|||||||
@@ -1,32 +1,81 @@
|
|||||||
import { brk } from "../utils/client.js";
|
import { brk } from "../utils/client.js";
|
||||||
import { createMapCache } from "../utils/cache.js";
|
import { createMapCache } from "../utils/cache.js";
|
||||||
import { latestPrice } from "../utils/price.js";
|
import { latestPrice } from "../utils/price.js";
|
||||||
import { formatBtc, renderRows, renderTx, TX_PAGE_SIZE } from "./render.js";
|
import { formatBtc, renderTx, showPanel, hidePanel, TX_PAGE_SIZE } from "./render.js";
|
||||||
|
|
||||||
/** @type {MapCache<Transaction[]>} */
|
/** @type {HTMLDivElement} */ let el;
|
||||||
const addrTxCache = createMapCache(200);
|
/** @type {HTMLSpanElement[]} */ let valueEls;
|
||||||
|
/** @type {HTMLDivElement} */ let txSection;
|
||||||
|
/** @type {string} */ let currentAddr = "";
|
||||||
|
|
||||||
|
const statsCache = createMapCache(50);
|
||||||
|
const txCache = createMapCache(200);
|
||||||
|
|
||||||
|
const ROW_LABELS = [
|
||||||
|
"Address",
|
||||||
|
"Confirmed Balance",
|
||||||
|
"Pending",
|
||||||
|
"Confirmed UTXOs",
|
||||||
|
"Pending UTXOs",
|
||||||
|
"Total Received",
|
||||||
|
"Tx Count",
|
||||||
|
"Type",
|
||||||
|
"Avg Cost Basis",
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @param {HTMLElement} parent @param {(e: MouseEvent) => void} linkHandler */
|
||||||
|
export function initAddrDetails(parent, linkHandler) {
|
||||||
|
el = document.createElement("div");
|
||||||
|
el.id = "addr-details";
|
||||||
|
el.hidden = true;
|
||||||
|
parent.append(el);
|
||||||
|
el.addEventListener("click", linkHandler);
|
||||||
|
|
||||||
|
const title = document.createElement("h1");
|
||||||
|
title.textContent = "Address";
|
||||||
|
el.append(title);
|
||||||
|
|
||||||
|
valueEls = ROW_LABELS.map((label) => {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.classList.add("row");
|
||||||
|
const labelEl = document.createElement("span");
|
||||||
|
labelEl.classList.add("label");
|
||||||
|
labelEl.textContent = label;
|
||||||
|
const valueEl = document.createElement("span");
|
||||||
|
valueEl.classList.add("value");
|
||||||
|
row.append(labelEl, valueEl);
|
||||||
|
el.append(row);
|
||||||
|
return valueEl;
|
||||||
|
});
|
||||||
|
|
||||||
|
txSection = document.createElement("div");
|
||||||
|
txSection.classList.add("transactions");
|
||||||
|
const heading = document.createElement("h2");
|
||||||
|
heading.textContent = "Transactions";
|
||||||
|
txSection.append(heading);
|
||||||
|
el.append(txSection);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} address
|
* @param {string} address
|
||||||
* @param {HTMLDivElement} el
|
* @param {AbortSignal} signal
|
||||||
* @param {{ signal: AbortSignal, cache: MapCache<AddrStats> }} options
|
|
||||||
*/
|
*/
|
||||||
export async function showAddrDetail(address, el, { signal, cache }) {
|
export async function update(address, signal) {
|
||||||
el.hidden = false;
|
currentAddr = address;
|
||||||
el.scrollTop = 0;
|
valueEls[0].textContent = address;
|
||||||
el.innerHTML = "";
|
for (let i = 1; i < valueEls.length; i++) {
|
||||||
|
valueEls[i].textContent = "...";
|
||||||
|
valueEls[i].classList.add("dim");
|
||||||
|
}
|
||||||
|
while (txSection.children.length > 1) txSection.lastChild?.remove();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cached = cache.get(address);
|
const cached = statsCache.get(address);
|
||||||
const stats = cached ?? (await brk.getAddress(address, { signal }));
|
const stats = cached ?? (await brk.getAddress(address, { signal }));
|
||||||
if (!cached) cache.set(address, stats);
|
if (!cached) statsCache.set(address, stats);
|
||||||
if (signal.aborted) return;
|
if (signal.aborted || currentAddr !== address) return;
|
||||||
|
|
||||||
const chain = stats.chainStats;
|
const chain = stats.chainStats;
|
||||||
|
|
||||||
const title = document.createElement("h1");
|
|
||||||
title.textContent = "Address";
|
|
||||||
el.append(title);
|
|
||||||
|
|
||||||
const balance = chain.fundedTxoSum - chain.spentTxoSum;
|
const balance = chain.fundedTxoSum - chain.spentTxoSum;
|
||||||
const mempool = stats.mempoolStats;
|
const mempool = stats.mempoolStats;
|
||||||
const pending = mempool ? mempool.fundedTxoSum - mempool.spentTxoSum : 0;
|
const pending = mempool ? mempool.fundedTxoSum - mempool.spentTxoSum : 0;
|
||||||
@@ -40,40 +89,26 @@ export async function showAddrDetail(address, el, { signal, cache }) {
|
|||||||
? ` $${((sats / 1e8) * price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
? ` $${((sats / 1e8) * price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
renderRows(
|
const values = [
|
||||||
[
|
address,
|
||||||
["Address", address],
|
`${formatBtc(balance)} BTC${fmtUsd(balance)}`,
|
||||||
["Confirmed Balance", `${formatBtc(balance)} BTC${fmtUsd(balance)}`],
|
`${pending >= 0 ? "+" : ""}${formatBtc(pending)} BTC${fmtUsd(pending)}`,
|
||||||
[
|
confirmedUtxos.toLocaleString(),
|
||||||
"Pending",
|
pendingUtxos.toLocaleString(),
|
||||||
`${pending >= 0 ? "+" : ""}${formatBtc(pending)} BTC${fmtUsd(pending)}`,
|
`${formatBtc(chain.fundedTxoSum)} BTC`,
|
||||||
],
|
chain.txCount.toLocaleString(),
|
||||||
["Confirmed UTXOs", confirmedUtxos.toLocaleString()],
|
(/** @type {any} */ (stats).addrType ?? "unknown")
|
||||||
["Pending UTXOs", pendingUtxos.toLocaleString()],
|
.replace(/^v\d+_/, "")
|
||||||
["Total Received", `${formatBtc(chain.fundedTxoSum)} BTC`],
|
.toUpperCase(),
|
||||||
["Tx Count", chain.txCount.toLocaleString()],
|
chain.realizedPrice
|
||||||
[
|
? `$${Number(chain.realizedPrice).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
||||||
"Type",
|
: "N/A",
|
||||||
/** @type {any} */ ((stats).addrType ?? "unknown")
|
];
|
||||||
.replace(/^v\d+_/, "")
|
|
||||||
.toUpperCase(),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Avg Cost Basis",
|
|
||||||
chain.realizedPrice
|
|
||||||
? `$${Number(chain.realizedPrice).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
|
||||||
: "N/A",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
el,
|
|
||||||
);
|
|
||||||
|
|
||||||
const section = document.createElement("div");
|
for (let i = 0; i < valueEls.length; i++) {
|
||||||
section.classList.add("transactions");
|
valueEls[i].textContent = values[i];
|
||||||
const heading = document.createElement("h2");
|
valueEls[i].classList.remove("dim");
|
||||||
heading.textContent = "Transactions";
|
}
|
||||||
section.append(heading);
|
|
||||||
el.append(section);
|
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let pageIndex = 0;
|
let pageIndex = 0;
|
||||||
@@ -81,35 +116,44 @@ export async function showAddrDetail(address, el, { signal, cache }) {
|
|||||||
let afterTxid;
|
let afterTxid;
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
if (entries[0].isIntersecting && !loading && pageIndex * TX_PAGE_SIZE < chain.txCount)
|
if (
|
||||||
|
entries[0].isIntersecting &&
|
||||||
|
!loading &&
|
||||||
|
pageIndex * TX_PAGE_SIZE < chain.txCount
|
||||||
|
)
|
||||||
loadMore();
|
loadMore();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadMore() {
|
async function loadMore() {
|
||||||
|
if (currentAddr !== address) return;
|
||||||
loading = true;
|
loading = true;
|
||||||
const key = `${address}:${pageIndex}`;
|
const key = `${address}:${pageIndex}`;
|
||||||
try {
|
try {
|
||||||
const cached = addrTxCache.get(key);
|
const cached = txCache.get(key);
|
||||||
const txs = cached ?? await brk.getAddressTxs(address, afterTxid, { signal });
|
const txs =
|
||||||
if (!cached) addrTxCache.set(key, txs);
|
cached ?? (await brk.getAddressTxs(address, afterTxid, { signal }));
|
||||||
for (const tx of txs) section.append(renderTx(tx));
|
if (!cached) txCache.set(key, txs);
|
||||||
|
if (currentAddr !== address) return;
|
||||||
|
for (const tx of txs) txSection.append(renderTx(tx));
|
||||||
pageIndex++;
|
pageIndex++;
|
||||||
if (txs.length) {
|
if (txs.length) {
|
||||||
afterTxid = txs[txs.length - 1].txid;
|
afterTxid = txs[txs.length - 1].txid;
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
const last = section.lastElementChild;
|
const last = txSection.lastElementChild;
|
||||||
if (last) observer.observe(last);
|
if (last) observer.observe(last);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("explorer addr txs:", e);
|
if (!signal.aborted) console.error("explorer addr txs:", e);
|
||||||
pageIndex = chain.txCount; // stop loading
|
pageIndex = chain.txCount;
|
||||||
}
|
}
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadMore();
|
await loadMore();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("explorer addr:", e);
|
if (!signal.aborted) console.error("explorer addr:", e);
|
||||||
el.textContent = "Address not found";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function show() { showPanel(el); }
|
||||||
|
export function hide() { hidePanel(el); }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { brk } from "../utils/client.js";
|
import { brk } from "../utils/client.js";
|
||||||
import { createMapCache } from "../utils/cache.js";
|
import { createMapCache } from "../utils/cache.js";
|
||||||
import { createPersistedValue } from "../utils/persisted.js";
|
import { createPersistedValue } from "../utils/persisted.js";
|
||||||
import { formatFeeRate, renderTx, TX_PAGE_SIZE } from "./render.js";
|
import { formatFeeRate, renderTx, showPanel, hidePanel, TX_PAGE_SIZE } from "./render.js";
|
||||||
|
|
||||||
/** @typedef {[string, (b: BlockInfoV1) => string | null, ((b: BlockInfoV1) => string | null)?]} RowDef */
|
/** @typedef {[string, (b: BlockInfoV1) => string | null, ((b: BlockInfoV1) => string | null)?]} RowDef */
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ export function initBlockDetails(parent, linkHandler) {
|
|||||||
const code = document.createElement("code");
|
const code = document.createElement("code");
|
||||||
const container = document.createElement("span");
|
const container = document.createElement("span");
|
||||||
heightPrefix = document.createElement("span");
|
heightPrefix = document.createElement("span");
|
||||||
heightPrefix.style.opacity = "0.5";
|
heightPrefix.classList.add("dim");
|
||||||
heightPrefix.style.userSelect = "none";
|
heightPrefix.style.userSelect = "none";
|
||||||
heightNum = document.createElement("span");
|
heightNum = document.createElement("span");
|
||||||
container.append(heightPrefix, heightNum);
|
container.append(heightPrefix, heightNum);
|
||||||
@@ -170,9 +170,6 @@ function updateTxNavs(page) {
|
|||||||
|
|
||||||
/** @param {BlockInfoV1} block */
|
/** @param {BlockInfoV1} block */
|
||||||
export function update(block) {
|
export function update(block) {
|
||||||
show();
|
|
||||||
el.scrollTop = 0;
|
|
||||||
|
|
||||||
const str = block.height.toString();
|
const str = block.height.toString();
|
||||||
heightPrefix.textContent = "#" + "0".repeat(7 - str.length);
|
heightPrefix.textContent = "#" + "0".repeat(7 - str.length);
|
||||||
heightNum.textContent = str;
|
heightNum.textContent = str;
|
||||||
@@ -200,13 +197,8 @@ export function update(block) {
|
|||||||
txObserver.observe(txSection);
|
txObserver.observe(txSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function show() {
|
export function show() { showPanel(el); }
|
||||||
el.hidden = false;
|
export function hide() { hidePanel(el); }
|
||||||
}
|
|
||||||
|
|
||||||
export function hide() {
|
|
||||||
el.hidden = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {number} page @param {boolean} [pushUrl] */
|
/** @param {number} page @param {boolean} [pushUrl] */
|
||||||
async function loadTxPage(page, pushUrl = true) {
|
async function loadTxPage(page, pushUrl = true) {
|
||||||
|
|||||||
@@ -56,11 +56,6 @@ export function initChain(parent, callbacks) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {string} hash */
|
|
||||||
export function getBlock(hash) {
|
|
||||||
return blocksByHash.get(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {string} hash */
|
/** @param {string} hash */
|
||||||
export function findCube(hash) {
|
export function findCube(hash) {
|
||||||
return /** @type {HTMLDivElement | null} */ (
|
return /** @type {HTMLDivElement | null} */ (
|
||||||
@@ -68,12 +63,13 @@ export function findCube(hash) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lastCube() {
|
export function deselectCube() {
|
||||||
return /** @type {HTMLDivElement | null} */ (blocksEl.lastElementChild);
|
if (selectedCube) selectedCube.classList.remove("selected");
|
||||||
|
selectedCube = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {HTMLDivElement} cube @param {{ scroll?: "smooth" | "instant" }} [opts] */
|
/** @param {HTMLDivElement} cube @param {{ scroll?: "smooth" | "instant", silent?: boolean }} [opts] */
|
||||||
export function selectCube(cube, { scroll } = {}) {
|
export function selectCube(cube, { scroll, silent } = {}) {
|
||||||
const changed = cube !== selectedCube;
|
const changed = cube !== selectedCube;
|
||||||
if (changed) {
|
if (changed) {
|
||||||
if (selectedCube) selectedCube.classList.remove("selected");
|
if (selectedCube) selectedCube.classList.remove("selected");
|
||||||
@@ -81,10 +77,12 @@ export function selectCube(cube, { scroll } = {}) {
|
|||||||
cube.classList.add("selected");
|
cube.classList.add("selected");
|
||||||
}
|
}
|
||||||
if (scroll) cube.scrollIntoView({ behavior: scroll });
|
if (scroll) cube.scrollIntoView({ behavior: scroll });
|
||||||
const hash = cube.dataset.hash;
|
if (!silent) {
|
||||||
if (hash) {
|
const hash = cube.dataset.hash;
|
||||||
const block = blocksByHash.get(hash);
|
if (hash) {
|
||||||
if (block) onSelect(block);
|
const block = blocksByHash.get(hash);
|
||||||
|
if (block) onSelect(block);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +125,7 @@ function appendNewerBlocks(blocks) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {number | null} [height] */
|
/** @param {number | null} [height] @returns {Promise<BlockHash>} */
|
||||||
export async function loadInitial(height) {
|
export async function loadInitial(height) {
|
||||||
const blocks =
|
const blocks =
|
||||||
height != null
|
height != null
|
||||||
@@ -140,6 +138,7 @@ export async function loadInitial(height) {
|
|||||||
reachedTip = height == null;
|
reachedTip = height == null;
|
||||||
observeOldestEdge();
|
observeOldestEdge();
|
||||||
if (!reachedTip) await loadNewer();
|
if (!reachedTip) await loadNewer();
|
||||||
|
return blocks[0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function poll() {
|
export async function poll() {
|
||||||
@@ -206,14 +205,14 @@ function createBlockCube(block) {
|
|||||||
const min = document.createElement("span");
|
const min = document.createElement("span");
|
||||||
min.innerHTML = formatFeeRate(feeRange[0]);
|
min.innerHTML = formatFeeRate(feeRange[0]);
|
||||||
const dash = document.createElement("span");
|
const dash = document.createElement("span");
|
||||||
dash.style.opacity = "0.5";
|
dash.classList.add("dim");
|
||||||
dash.innerHTML = `-`;
|
dash.innerHTML = `-`;
|
||||||
const max = document.createElement("span");
|
const max = document.createElement("span");
|
||||||
max.innerHTML = formatFeeRate(feeRange[6]);
|
max.innerHTML = formatFeeRate(feeRange[6]);
|
||||||
range.append(min, dash, max);
|
range.append(min, dash, max);
|
||||||
feesEl.append(range);
|
feesEl.append(range);
|
||||||
const unit = document.createElement("p");
|
const unit = document.createElement("p");
|
||||||
unit.style.opacity = "0.5";
|
unit.classList.add("dim");
|
||||||
unit.innerHTML = `sat/vB`;
|
unit.innerHTML = `sat/vB`;
|
||||||
feesEl.append(unit);
|
feesEl.append(unit);
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
loadInitial,
|
loadInitial,
|
||||||
poll,
|
poll,
|
||||||
selectCube,
|
selectCube,
|
||||||
|
deselectCube,
|
||||||
findCube,
|
findCube,
|
||||||
lastCube,
|
|
||||||
clear as clearChain,
|
clear as clearChain,
|
||||||
} from "./chain.js";
|
} from "./chain.js";
|
||||||
import {
|
import {
|
||||||
@@ -16,20 +16,28 @@ import {
|
|||||||
show as showBlock,
|
show as showBlock,
|
||||||
hide as hideBlock,
|
hide as hideBlock,
|
||||||
} from "./block.js";
|
} from "./block.js";
|
||||||
import { showTxFromData } from "./tx.js";
|
import {
|
||||||
import { showAddrDetail } from "./address.js";
|
initTxDetails,
|
||||||
|
update as updateTx,
|
||||||
|
clear as clearTx,
|
||||||
|
show as showTx,
|
||||||
|
hide as hideTx,
|
||||||
|
} from "./tx.js";
|
||||||
|
import {
|
||||||
|
initAddrDetails,
|
||||||
|
update as updateAddr,
|
||||||
|
show as showAddr,
|
||||||
|
hide as hideAddr,
|
||||||
|
} from "./address.js";
|
||||||
|
|
||||||
/** @returns {string[]} */
|
/** @returns {string[]} */
|
||||||
function pathSegments() {
|
function pathSegments() {
|
||||||
return window.location.pathname.split("/").filter((v) => v);
|
return window.location.pathname.split("/").filter((v) => v);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {HTMLDivElement} */ let secondaryPanel;
|
|
||||||
/** @type {number | undefined} */ let pollInterval;
|
/** @type {number | undefined} */ let pollInterval;
|
||||||
/** @type {Transaction | null} */ let pendingTx = null;
|
|
||||||
let navController = new AbortController();
|
let navController = new AbortController();
|
||||||
const txCache = createMapCache(50);
|
const txCache = createMapCache(50);
|
||||||
const addrCache = createMapCache(50);
|
|
||||||
|
|
||||||
function navigate() {
|
function navigate() {
|
||||||
navController.abort();
|
navController.abort();
|
||||||
@@ -37,14 +45,10 @@ function navigate() {
|
|||||||
return navController.signal;
|
return navController.signal;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showBlockPanel() {
|
function showPanel(/** @type {"block" | "tx" | "addr"} */ which) {
|
||||||
showBlock();
|
which === "block" ? showBlock() : hideBlock();
|
||||||
secondaryPanel.hidden = true;
|
which === "tx" ? showTx() : hideTx();
|
||||||
}
|
which === "addr" ? showAddr() : hideAddr();
|
||||||
|
|
||||||
function showSecondaryPanel() {
|
|
||||||
hideBlock();
|
|
||||||
secondaryPanel.hidden = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {MouseEvent} e */
|
/** @param {MouseEvent} e */
|
||||||
@@ -71,9 +75,10 @@ export function init() {
|
|||||||
initChain(explorerElement, {
|
initChain(explorerElement, {
|
||||||
onSelect: (block) => {
|
onSelect: (block) => {
|
||||||
updateBlock(block);
|
updateBlock(block);
|
||||||
showBlockPanel();
|
showPanel("block");
|
||||||
},
|
},
|
||||||
onCubeClick: (cube) => {
|
onCubeClick: (cube) => {
|
||||||
|
navigate();
|
||||||
const hash = cube.dataset.hash;
|
const hash = cube.dataset.hash;
|
||||||
if (hash) history.pushState(null, "", `/block/${hash}`);
|
if (hash) history.pushState(null, "", `/block/${hash}`);
|
||||||
selectCube(cube);
|
selectCube(cube);
|
||||||
@@ -81,12 +86,8 @@ export function init() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
initBlockDetails(explorerElement, handleLinkClick);
|
initBlockDetails(explorerElement, handleLinkClick);
|
||||||
|
initTxDetails(explorerElement, handleLinkClick);
|
||||||
secondaryPanel = document.createElement("div");
|
initAddrDetails(explorerElement, handleLinkClick);
|
||||||
secondaryPanel.id = "tx-details";
|
|
||||||
secondaryPanel.hidden = true;
|
|
||||||
explorerElement.append(secondaryPanel);
|
|
||||||
secondaryPanel.addEventListener("click", handleLinkClick);
|
|
||||||
|
|
||||||
new MutationObserver(() => {
|
new MutationObserver(() => {
|
||||||
if (explorerElement.hidden) stopPolling();
|
if (explorerElement.hidden) stopPolling();
|
||||||
@@ -105,7 +106,7 @@ export function init() {
|
|||||||
if (kind === "block" && value) navigateToBlock(value, false);
|
if (kind === "block" && value) navigateToBlock(value, false);
|
||||||
else if (kind === "tx" && value) navigateToTx(value);
|
else if (kind === "tx" && value) navigateToTx(value);
|
||||||
else if (kind === "address" && value) navigateToAddr(value);
|
else if (kind === "address" && value) navigateToAddr(value);
|
||||||
else showBlockPanel();
|
else showPanel("block");
|
||||||
});
|
});
|
||||||
|
|
||||||
load();
|
load();
|
||||||
@@ -126,116 +127,97 @@ function stopPolling() {
|
|||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const height = await resolveStartHeight();
|
const [kind, value] = pathSegments();
|
||||||
await loadInitial(height);
|
|
||||||
route();
|
if (kind === "tx" && value) {
|
||||||
|
const tx = txCache.get(value) ?? (await brk.getTx(value));
|
||||||
|
txCache.set(value, tx);
|
||||||
|
const startHash = await loadInitial(tx.status?.blockHeight ?? null);
|
||||||
|
const cube = tx.status?.blockHash ? findCube(tx.status.blockHash) : findCube(startHash);
|
||||||
|
if (cube) selectCube(cube, { silent: true });
|
||||||
|
updateTx(tx);
|
||||||
|
showPanel("tx");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kind === "address" && value) {
|
||||||
|
const startHash = await loadInitial(null);
|
||||||
|
const cube = findCube(startHash);
|
||||||
|
if (cube) selectCube(cube, { silent: true });
|
||||||
|
navigateToAddr(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const height =
|
||||||
|
kind === "block" && value
|
||||||
|
? /^\d+$/.test(value)
|
||||||
|
? Number(value)
|
||||||
|
: (await brk.getBlockV1(value)).height
|
||||||
|
: null;
|
||||||
|
const startHash = await loadInitial(height);
|
||||||
|
const cube = findCube(startHash);
|
||||||
|
if (cube) selectCube(cube, { scroll: "instant" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("explorer load:", e);
|
console.error("explorer load:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {AbortSignal} [signal] @returns {Promise<number | null>} */
|
|
||||||
async function resolveStartHeight(signal) {
|
|
||||||
const [kind, value] = pathSegments();
|
|
||||||
if (!value) return null;
|
|
||||||
if (kind === "block") {
|
|
||||||
if (/^\d+$/.test(value)) return Number(value);
|
|
||||||
return (await brk.getBlockV1(value, { signal })).height;
|
|
||||||
}
|
|
||||||
if (kind === "tx") {
|
|
||||||
const tx = txCache.get(value) ?? (await brk.getTx(value, { signal }));
|
|
||||||
txCache.set(value, tx);
|
|
||||||
pendingTx = tx;
|
|
||||||
return tx.status?.blockHeight ?? null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function route() {
|
|
||||||
const [kind, value] = pathSegments();
|
|
||||||
if (pendingTx) {
|
|
||||||
const hash = pendingTx.status?.blockHash;
|
|
||||||
const cube = hash ? findCube(hash) : null;
|
|
||||||
if (cube) selectCube(cube, { scroll: "instant" });
|
|
||||||
showTxFromData(pendingTx, secondaryPanel);
|
|
||||||
showSecondaryPanel();
|
|
||||||
pendingTx = null;
|
|
||||||
} else if (kind === "address" && value) {
|
|
||||||
const cube = lastCube();
|
|
||||||
if (cube) selectCube(cube, { scroll: "instant" });
|
|
||||||
navigateToAddr(value);
|
|
||||||
} else {
|
|
||||||
const cube = lastCube();
|
|
||||||
if (cube) selectCube(cube, { scroll: "instant" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {string} hash @param {boolean} [pushUrl] */
|
/** @param {string} hash @param {boolean} [pushUrl] */
|
||||||
async function navigateToBlock(hash, pushUrl = true) {
|
async function navigateToBlock(hash, pushUrl = true) {
|
||||||
if (pushUrl) history.pushState(null, "", `/block/${hash}`);
|
if (pushUrl) history.pushState(null, "", `/block/${hash}`);
|
||||||
const cube = findCube(hash);
|
const existing = findCube(hash);
|
||||||
if (cube) {
|
if (existing) {
|
||||||
selectCube(cube, { scroll: "smooth" });
|
navigate();
|
||||||
} else {
|
selectCube(existing, { scroll: "smooth" });
|
||||||
const signal = navigate();
|
return;
|
||||||
try {
|
}
|
||||||
clearChain();
|
const signal = navigate();
|
||||||
await loadInitial(await resolveStartHeight(signal));
|
try {
|
||||||
if (signal.aborted) return;
|
clearChain();
|
||||||
route();
|
const height = /^\d+$/.test(hash)
|
||||||
} catch (e) {
|
? Number(hash)
|
||||||
if (!signal.aborted) console.error("explorer block:", e);
|
: (await brk.getBlockV1(hash, { signal })).height;
|
||||||
}
|
if (signal.aborted) return;
|
||||||
|
const startHash = await loadInitial(height);
|
||||||
|
if (signal.aborted) return;
|
||||||
|
const cube = findCube(hash) ?? findCube(startHash);
|
||||||
|
if (cube) selectCube(cube);
|
||||||
|
} catch (e) {
|
||||||
|
if (!signal.aborted) console.error("explorer block:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {string} txid */
|
/** @param {string} txid */
|
||||||
async function navigateToTx(txid) {
|
async function navigateToTx(txid) {
|
||||||
const cached = txCache.get(txid);
|
|
||||||
if (cached) {
|
|
||||||
navigate();
|
|
||||||
showTxAndSelectBlock(cached);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const signal = navigate();
|
const signal = navigate();
|
||||||
|
clearTx();
|
||||||
|
showPanel("tx");
|
||||||
try {
|
try {
|
||||||
const tx = await brk.getTx(txid, {
|
const tx = txCache.get(txid) ?? (await brk.getTx(txid, { signal }));
|
||||||
signal,
|
if (signal.aborted) return;
|
||||||
onUpdate: (tx) => {
|
|
||||||
txCache.set(txid, tx);
|
|
||||||
if (!signal.aborted) showTxAndSelectBlock(tx);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
txCache.set(txid, tx);
|
txCache.set(txid, tx);
|
||||||
|
|
||||||
|
if (tx.status?.blockHash) {
|
||||||
|
let cube = findCube(tx.status.blockHash);
|
||||||
|
if (!cube) {
|
||||||
|
clearChain();
|
||||||
|
const startHash = await loadInitial(tx.status.blockHeight ?? null);
|
||||||
|
if (signal.aborted) return;
|
||||||
|
cube = findCube(tx.status.blockHash) ?? findCube(startHash);
|
||||||
|
}
|
||||||
|
if (cube) selectCube(cube, { scroll: "smooth", silent: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTx(tx);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!signal.aborted) console.error("explorer tx:", e);
|
if (!signal.aborted) console.error("explorer tx:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {Transaction} tx */
|
|
||||||
function showTxAndSelectBlock(tx) {
|
|
||||||
if (tx.status?.blockHash) {
|
|
||||||
const cube = findCube(tx.status.blockHash);
|
|
||||||
if (cube) {
|
|
||||||
selectCube(cube, { scroll: "smooth" });
|
|
||||||
showTxFromData(tx, secondaryPanel);
|
|
||||||
showSecondaryPanel();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pendingTx = tx;
|
|
||||||
clearChain();
|
|
||||||
loadInitial(tx.status.blockHeight ?? null).then(() => {
|
|
||||||
if (!navController.signal.aborted) route();
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showTxFromData(tx, secondaryPanel);
|
|
||||||
showSecondaryPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {string} address */
|
/** @param {string} address */
|
||||||
function navigateToAddr(address) {
|
function navigateToAddr(address) {
|
||||||
const signal = navigate();
|
navigate();
|
||||||
showAddrDetail(address, secondaryPanel, { signal, cache: addrCache });
|
deselectCube();
|
||||||
showSecondaryPanel();
|
updateAddr(address, navController.signal);
|
||||||
|
showPanel("addr");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
export const TX_PAGE_SIZE = 25;
|
export const TX_PAGE_SIZE = 25;
|
||||||
|
|
||||||
|
/** @param {HTMLElement} el */
|
||||||
|
export function showPanel(el) {
|
||||||
|
el.hidden = false;
|
||||||
|
el.scrollTop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {HTMLElement} el */
|
||||||
|
export function hidePanel(el) {
|
||||||
|
el.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
/** @param {number} sats */
|
/** @param {number} sats */
|
||||||
export function formatBtc(sats) {
|
export function formatBtc(sats) {
|
||||||
return (sats / 1e8).toFixed(8);
|
return (sats / 1e8).toFixed(8);
|
||||||
@@ -33,7 +44,7 @@ export function createHeightElement(height) {
|
|||||||
const container = document.createElement("span");
|
const container = document.createElement("span");
|
||||||
const str = height.toString();
|
const str = height.toString();
|
||||||
const prefix = document.createElement("span");
|
const prefix = document.createElement("span");
|
||||||
prefix.style.opacity = "0.5";
|
prefix.classList.add("dim");
|
||||||
prefix.style.userSelect = "none";
|
prefix.style.userSelect = "none";
|
||||||
prefix.textContent = "#" + "0".repeat(7 - str.length);
|
prefix.textContent = "#" + "0".repeat(7 - str.length);
|
||||||
const num = document.createElement("span");
|
const num = document.createElement("span");
|
||||||
@@ -62,12 +73,6 @@ export function renderRows(rows, parent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Transaction} tx
|
|
||||||
* @param {string} [coinbaseAscii]
|
|
||||||
*/
|
|
||||||
const IO_LIMIT = 10;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {TxIn} vin
|
* @param {TxIn} vin
|
||||||
* @param {string} [coinbaseAscii]
|
* @param {string} [coinbaseAscii]
|
||||||
@@ -142,16 +147,16 @@ function renderOutput(vout) {
|
|||||||
* @param {(item: T) => HTMLElement} render
|
* @param {(item: T) => HTMLElement} render
|
||||||
* @param {HTMLElement} container
|
* @param {HTMLElement} container
|
||||||
*/
|
*/
|
||||||
function renderCapped(items, render, container) {
|
function renderCapped(items, render, container, max = 10) {
|
||||||
const limit = Math.min(items.length, IO_LIMIT);
|
const limit = Math.min(items.length, max);
|
||||||
for (let i = 0; i < limit; i++) container.append(render(items[i]));
|
for (let i = 0; i < limit; i++) container.append(render(items[i]));
|
||||||
if (items.length > IO_LIMIT) {
|
if (items.length > max) {
|
||||||
const btn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
btn.classList.add("show-more");
|
btn.classList.add("show-more");
|
||||||
btn.textContent = `Show ${items.length - IO_LIMIT} more`;
|
btn.textContent = `Show ${items.length - max} more`;
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
btn.remove();
|
btn.remove();
|
||||||
for (let i = IO_LIMIT; i < items.length; i++) container.append(render(items[i]));
|
for (let i = max; i < items.length; i++) container.append(render(items[i]));
|
||||||
});
|
});
|
||||||
container.append(btn);
|
container.append(btn);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,34 @@
|
|||||||
import { formatBtc, formatFeeRate, renderRows, renderTx } from "./render.js";
|
import { formatBtc, formatFeeRate, renderRows, renderTx, showPanel, hidePanel } from "./render.js";
|
||||||
|
|
||||||
/**
|
/** @type {HTMLDivElement} */ let el;
|
||||||
* @param {Transaction} tx
|
|
||||||
* @param {HTMLDivElement} el
|
/** @param {HTMLElement} parent @param {(e: MouseEvent) => void} linkHandler */
|
||||||
*/
|
export function initTxDetails(parent, linkHandler) {
|
||||||
export function showTxFromData(tx, el) {
|
el = document.createElement("div");
|
||||||
el.hidden = false;
|
el.id = "tx-details";
|
||||||
el.scrollTop = 0;
|
el.hidden = true;
|
||||||
|
parent.append(el);
|
||||||
|
el.addEventListener("click", linkHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function show() { showPanel(el); }
|
||||||
|
export function hide() { hidePanel(el); }
|
||||||
|
|
||||||
|
export function clear() {
|
||||||
|
if (el.children.length) {
|
||||||
|
el.querySelector(".transactions")?.remove();
|
||||||
|
for (const v of el.querySelectorAll(".row .value")) {
|
||||||
|
v.classList.add("dim");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const title = document.createElement("h1");
|
||||||
|
title.textContent = "Transaction";
|
||||||
|
el.append(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Transaction} tx */
|
||||||
|
export function update(tx) {
|
||||||
el.innerHTML = "";
|
el.innerHTML = "";
|
||||||
|
|
||||||
const title = document.createElement("h1");
|
const title = document.createElement("h1");
|
||||||
|
|||||||
@@ -10,15 +10,22 @@ function processPathname(pathname) {
|
|||||||
|
|
||||||
const chartParamsWhitelist = ["range"];
|
const chartParamsWhitelist = ["range"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | string[]} [pathname]
|
||||||
|
* @param {URLSearchParams} [urlParams]
|
||||||
|
*/
|
||||||
|
function buildUrl(pathname, urlParams) {
|
||||||
|
const path = processPathname(pathname);
|
||||||
|
const query = (urlParams ?? new URLSearchParams(window.location.search)).toString();
|
||||||
|
return `/${path}${query ? `?${query}` : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string | string[]} pathname
|
* @param {string | string[]} pathname
|
||||||
*/
|
*/
|
||||||
export function pushHistory(pathname) {
|
export function pushHistory(pathname) {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
pathname = processPathname(pathname);
|
|
||||||
try {
|
try {
|
||||||
const url = `/${pathname}?${urlParams.toString()}`;
|
window.history.pushState(null, "", buildUrl(pathname));
|
||||||
window.history.pushState(null, "", url);
|
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,11 +35,8 @@ export function pushHistory(pathname) {
|
|||||||
* @param {string | string[]} [args.pathname]
|
* @param {string | string[]} [args.pathname]
|
||||||
*/
|
*/
|
||||||
export function replaceHistory({ urlParams, pathname }) {
|
export function replaceHistory({ urlParams, pathname }) {
|
||||||
urlParams ||= new URLSearchParams(window.location.search);
|
|
||||||
pathname = processPathname(pathname);
|
|
||||||
try {
|
try {
|
||||||
const url = `/${pathname}?${urlParams.toString()}`;
|
window.history.replaceState(null, "", buildUrl(pathname, urlParams));
|
||||||
window.history.replaceState(null, "", url);
|
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ main {
|
|||||||
|
|
||||||
> fieldset {
|
> fieldset {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1.25rem;
|
gap: 1.125rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.dim {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: var(--main-padding) 0;
|
padding: var(--main-padding) 0;
|
||||||
@@ -180,7 +184,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#block-details,
|
#block-details,
|
||||||
#tx-details {
|
#tx-details,
|
||||||
|
#addr-details {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
line-height: var(--line-height-sm);
|
line-height: var(--line-height-sm);
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
#simulation {
|
|
||||||
min-height: 0;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2rem;
|
|
||||||
padding: var(--main-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
> div:first-child {
|
|
||||||
border-bottom: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-bottom: var(--bottom-area);
|
|
||||||
}
|
|
||||||
|
|
||||||
> div:first-child {
|
|
||||||
max-width: var(--default-main-width);
|
|
||||||
border-right: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
> div:last-child {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem;
|
|
||||||
overflow-x: hidden;
|
|
||||||
|
|
||||||
p {
|
|
||||||
text-wrap: pretty;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
> span {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
small {
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
line-height: var(--line-height-sm);
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart {
|
|
||||||
flex: none;
|
|
||||||
height: 400px;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
margin-left: calc(var(--negative-main-padding) * 0.75);
|
|
||||||
|
|
||||||
fieldset {
|
|
||||||
margin-left: -0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
#table {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2rem;
|
|
||||||
padding: var(--main-padding);
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
line-height: var(--line-height-xs);
|
|
||||||
font-weight: 450;
|
|
||||||
margin-left: var(--negative-main-padding);
|
|
||||||
margin-right: var(--negative-main-padding);
|
|
||||||
|
|
||||||
table {
|
|
||||||
z-index: 10;
|
|
||||||
border-top-width: 1px;
|
|
||||||
border-style: dashed !important;
|
|
||||||
line-height: var(--line-height-sm);
|
|
||||||
text-transform: uppercase;
|
|
||||||
table-layout: auto;
|
|
||||||
border-collapse: separate;
|
|
||||||
border-spacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
border-right: 1px;
|
|
||||||
border-bottom: 1px;
|
|
||||||
border-color: var(--off-color);
|
|
||||||
border-style: dashed !important;
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
text-transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
margin: -0.2rem 0;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
th:first-child {
|
|
||||||
padding-left: var(--main-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
th[scope="col"] {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background-color: var(--background-color);
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding-top: 0.275rem;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.25rem;
|
|
||||||
text-transform: lowercase;
|
|
||||||
color: var(--off-color);
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
> span {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
> button {
|
|
||||||
padding: 0 0.25rem;
|
|
||||||
margin: 0 -0.25rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(2) {
|
|
||||||
button:nth-of-type(1) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
button:nth-of-type(2) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
margin-right: -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
> button {
|
|
||||||
padding: 1rem;
|
|
||||||
min-width: 10rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
border-top-width: 1px;
|
|
||||||
width: 100%;
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
border-style: dashed !important;
|
|
||||||
|
|
||||||
> span {
|
|
||||||
text-align: left;
|
|
||||||
position: sticky;
|
|
||||||
top: 2rem;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user