From db0298ac1b9968dca6bff3aff4a45b77c22cc8bd Mon Sep 17 00:00:00 2001 From: nym21 Date: Mon, 13 Oct 2025 13:52:33 +0200 Subject: [PATCH] global: snapshot --- Cargo.lock | 13 +- Cargo.toml | 2 +- crates/brk_computer/examples/pools.rs | 11 +- crates/brk_computer/src/pools/mod.rs | 11 +- crates/brk_error/src/lib.rs | 8 + crates/brk_interface/src/chain/addresses.rs | 37 +- .../brk_interface/src/chain/transactions.rs | 19 +- crates/brk_interface/src/lib.rs | 20 +- .../brk_interface/src/{metrics => }/output.rs | 0 crates/brk_interface/src/params.rs | 29 +- crates/brk_interface/src/vecs.rs | 6 +- crates/brk_mempool/README.md | 1 - crates/brk_mempool/src/lib.rs | 71 ---- .../{brk_mempool => brk_monitor}/Cargo.toml | 8 +- crates/brk_monitor/README.md | 30 ++ crates/{brk_mempool => brk_monitor}/build.rs | 0 crates/brk_monitor/src/lib.rs | 70 ++++ .../{brk_mempool => brk_monitor}/src/main.rs | 2 +- crates/brk_server/src/api/addresses/mod.rs | 8 +- crates/brk_server/src/api/metrics/mod.rs | 21 +- crates/brk_server/src/api/transactions/mod.rs | 4 +- crates/brk_structs/Cargo.toml | 1 + crates/brk_structs/src/address.rs | 112 ++++++ crates/brk_structs/src/addressbytes.rs | 346 +----------------- crates/brk_structs/src/addresschainstats.rs | 33 ++ crates/brk_structs/src/addressinfo.rs | 69 ---- crates/brk_structs/src/addressmempoolstats.rs | 31 ++ crates/brk_structs/src/addresspath.rs | 9 - crates/brk_structs/src/addressstats.rs | 19 + crates/brk_structs/src/blockhash.rs | 6 +- crates/brk_structs/src/bytes.rs | 113 ++++++ crates/brk_structs/src/height.rs | 3 + crates/brk_structs/src/index.rs | 2 +- crates/brk_structs/src/lib.rs | 52 ++- .../src/{metricsearchquery.rs => limit.rs} | 16 +- crates/brk_structs/src/metric.rs | 32 ++ crates/brk_structs/src/metricpath.rs | 9 - .../mod.rs => brk_structs/src/metrics.rs} | 72 ++-- crates/brk_structs/src/p2abytes.rs | 30 ++ crates/brk_structs/src/p2pk33bytes.rs | 30 ++ crates/brk_structs/src/p2pk65bytes.rs | 30 ++ crates/brk_structs/src/p2pkhbytes.rs | 30 ++ crates/brk_structs/src/p2shbytes.rs | 30 ++ crates/brk_structs/src/p2trbytes.rs | 30 ++ crates/brk_structs/src/p2wpkhbytes.rs | 30 ++ crates/brk_structs/src/p2wshbytes.rs | 30 ++ crates/brk_structs/src/rawlocktime.rs | 13 +- crates/brk_structs/src/sats.rs | 1 + crates/brk_structs/src/timestamp.rs | 3 + crates/brk_structs/src/tx.rs | 43 +++ crates/brk_structs/src/txid.rs | 6 + crates/brk_structs/src/txinfo.rs | 19 - crates/brk_structs/src/txinput.rs | 40 ++ crates/brk_structs/src/txoutput.rs | 27 ++ crates/brk_structs/src/txprevout.rs | 27 ++ crates/brk_structs/src/txstatus.rs | 24 ++ crates/brk_structs/src/txversion.rs | 3 + crates/brk_structs/src/vout.rs | 5 +- 58 files changed, 1094 insertions(+), 653 deletions(-) rename crates/brk_interface/src/{metrics => }/output.rs (100%) delete mode 100644 crates/brk_mempool/README.md delete mode 100644 crates/brk_mempool/src/lib.rs rename crates/{brk_mempool => brk_monitor}/Cargo.toml (66%) create mode 100644 crates/brk_monitor/README.md rename crates/{brk_mempool => brk_monitor}/build.rs (100%) create mode 100644 crates/brk_monitor/src/lib.rs rename crates/{brk_mempool => brk_monitor}/src/main.rs (97%) create mode 100644 crates/brk_structs/src/address.rs create mode 100644 crates/brk_structs/src/addresschainstats.rs delete mode 100644 crates/brk_structs/src/addressinfo.rs create mode 100644 crates/brk_structs/src/addressmempoolstats.rs delete mode 100644 crates/brk_structs/src/addresspath.rs create mode 100644 crates/brk_structs/src/addressstats.rs create mode 100644 crates/brk_structs/src/bytes.rs rename crates/brk_structs/src/{metricsearchquery.rs => limit.rs} (52%) create mode 100644 crates/brk_structs/src/metric.rs delete mode 100644 crates/brk_structs/src/metricpath.rs rename crates/{brk_interface/src/metrics/mod.rs => brk_structs/src/metrics.rs} (52%) create mode 100644 crates/brk_structs/src/p2abytes.rs create mode 100644 crates/brk_structs/src/p2pk33bytes.rs create mode 100644 crates/brk_structs/src/p2pk65bytes.rs create mode 100644 crates/brk_structs/src/p2pkhbytes.rs create mode 100644 crates/brk_structs/src/p2shbytes.rs create mode 100644 crates/brk_structs/src/p2trbytes.rs create mode 100644 crates/brk_structs/src/p2wpkhbytes.rs create mode 100644 crates/brk_structs/src/p2wshbytes.rs create mode 100644 crates/brk_structs/src/tx.rs delete mode 100644 crates/brk_structs/src/txinfo.rs create mode 100644 crates/brk_structs/src/txinput.rs create mode 100644 crates/brk_structs/src/txoutput.rs create mode 100644 crates/brk_structs/src/txprevout.rs create mode 100644 crates/brk_structs/src/txstatus.rs diff --git a/Cargo.lock b/Cargo.lock index 4d3c23447..2175f3f88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -751,7 +751,7 @@ dependencies = [ ] [[package]] -name = "brk_mempool" +name = "brk_monitor" version = "0.0.111" dependencies = [ "bitcoin", @@ -1241,6 +1241,7 @@ dependencies = [ "schemars", "serde", "serde_bytes", + "serde_json", "strum", "vecdb", "zerocopy", @@ -2113,9 +2114,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "1dc8f7d2ded5f9209535e4b3fd4d39c002f30902ff5ce9f64e2c33d549576500" dependencies = [ "typenum", "version_check", @@ -4410,12 +4411,12 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4f2d25788..7a9503fdd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ brk_indexer = { version = "0.0.111", path = "crates/brk_indexer" } brk_interface = { version = "0.0.111", path = "crates/brk_interface" } brk_logger = { version = "0.0.111", path = "crates/brk_logger" } brk_mcp = { version = "0.0.111", path = "crates/brk_mcp" } -brk_mempool = { version = "0.0.111", path = "crates/brk_mempool" } +brk_monitor = { version = "0.0.111", path = "crates/brk_monitor" } brk_parser = { version = "0.0.111", path = "crates/brk_parser" } brk_server = { version = "0.0.111", path = "crates/brk_server" } brk_store = { version = "0.0.111", path = "crates/brk_store" } diff --git a/crates/brk_computer/examples/pools.rs b/crates/brk_computer/examples/pools.rs index 903fff757..8c603ed70 100644 --- a/crates/brk_computer/examples/pools.rs +++ b/crates/brk_computer/examples/pools.rs @@ -4,7 +4,7 @@ use brk_computer::Computer; use brk_error::Result; use brk_fetcher::Fetcher; use brk_indexer::Indexer; -use brk_structs::{AddressBytes, OutputIndex, OutputType, pools}; +use brk_structs::{Address, AddressBytes, OutputIndex, OutputType, pools}; use vecdb::{AnyIterableVec, Exit, VecIterator}; fn main() -> Result<()> { @@ -68,7 +68,7 @@ fn main() -> Result<()> { let typeindex = outputindex_to_typeindex_iter.unwrap_get_inner(outputindex); - let address = match outputtype { + match outputtype { OutputType::P2PK65 => Some(AddressBytes::from( p2pk65addressindex_to_p2pk65bytes_iter .unwrap_get_inner(typeindex.into()), @@ -102,10 +102,9 @@ fn main() -> Result<()> { .unwrap_get_inner(typeindex.into()), )), _ => None, - }; - - address - .and_then(|address| pools.find_from_address(&address.to_string())) + } + .map(|bytes| Address::try_from(bytes).unwrap()) + .and_then(|address| pools.find_from_address(&address)) }) .or_else(|| pools.find_from_coinbase_tag(&coinbase_tag)) .unwrap_or(unknown); diff --git a/crates/brk_computer/src/pools/mod.rs b/crates/brk_computer/src/pools/mod.rs index 508d11dae..21598ac49 100644 --- a/crates/brk_computer/src/pools/mod.rs +++ b/crates/brk_computer/src/pools/mod.rs @@ -4,7 +4,7 @@ use allocative::Allocative; use brk_error::Result; use brk_indexer::Indexer; use brk_store::AnyStore; -use brk_structs::{AddressBytes, Height, OutputIndex, OutputType, PoolId, Pools, pools}; +use brk_structs::{Address, AddressBytes, Height, OutputIndex, OutputType, PoolId, Pools, pools}; use brk_traversable::Traversable; use rayon::prelude::*; use vecdb::{ @@ -167,7 +167,7 @@ impl Vecs { outputindex_to_outputtype_iter.unwrap_get_inner(outputindex); let typeindex = outputindex_to_typeindex_iter.unwrap_get_inner(outputindex); - let address = match outputtype { + match outputtype { OutputType::P2PK65 => Some(AddressBytes::from( p2pk65addressindex_to_p2pk65bytes_iter .unwrap_get_inner(typeindex.into()), @@ -200,10 +200,9 @@ impl Vecs { p2aaddressindex_to_p2abytes_iter.unwrap_get_inner(typeindex.into()), )), _ => None, - }; - - address - .and_then(|address| self.pools.find_from_address(&address.to_string())) + } + .map(|bytes| Address::try_from(bytes).unwrap()) + .and_then(|address| self.pools.find_from_address(&address)) }) .or_else(|| self.pools.find_from_coinbase_tag(&coinbase_tag)) .unwrap_or(unknown); diff --git a/crates/brk_error/src/lib.rs b/crates/brk_error/src/lib.rs index 1530c0bfd..7410ef056 100644 --- a/crates/brk_error/src/lib.rs +++ b/crates/brk_error/src/lib.rs @@ -19,6 +19,7 @@ pub enum Error { SystemTimeError(time::SystemTimeError), BitcoinConsensusEncode(bitcoin::consensus::encode::Error), BitcoinBip34Error(bitcoin::block::Bip34Error), + BitcoinFromScriptError(bitcoin::address::FromScriptError), SonicRS(sonic_rs::Error), ZeroCopyError, Vecs(vecdb::Error), @@ -51,6 +52,12 @@ impl From for Error { } } +impl From for Error { + fn from(value: bitcoin::address::FromScriptError) -> Self { + Self::BitcoinFromScriptError(value) + } +} + impl From for Error { fn from(value: time::SystemTimeError) -> Self { Self::SystemTimeError(value) @@ -128,6 +135,7 @@ impl fmt::Display for Error { match self { Error::BitcoinConsensusEncode(error) => Display::fmt(&error, f), Error::BitcoinBip34Error(error) => Display::fmt(&error, f), + Error::BitcoinFromScriptError(error) => Display::fmt(&error, f), Error::BitcoinRPC(error) => Display::fmt(&error, f), Error::Fjall(error) => Display::fmt(&error, f), Error::IO(error) => Display::fmt(&error, f), diff --git a/crates/brk_interface/src/chain/addresses.rs b/crates/brk_interface/src/chain/addresses.rs index 6859d7fe6..824a109c2 100644 --- a/crates/brk_interface/src/chain/addresses.rs +++ b/crates/brk_interface/src/chain/addresses.rs @@ -1,24 +1,21 @@ use std::str::FromStr; -use bitcoin::{Address, Network, PublicKey, ScriptBuf}; +use bitcoin::{Network, PublicKey, ScriptBuf}; use brk_error::{Error, Result}; use brk_structs::{ - AddressBytes, AddressBytesHash, AddressInfo, AddressPath, AnyAddressDataIndexEnum, Bitcoin, + Address, AddressBytes, AddressBytesHash, AddressStats, AnyAddressDataIndexEnum, Bitcoin, OutputType, }; use vecdb::{AnyIterableVec, VecIterator}; use crate::Interface; -pub fn get_address_info( - AddressPath { address }: AddressPath, - interface: &Interface, -) -> Result { +pub fn get_address(Address { address }: Address, interface: &Interface) -> Result { let indexer = interface.indexer(); let computer = interface.computer(); let stores = &indexer.stores; - let script = if let Ok(address) = Address::from_str(&address) { + let script = if let Ok(address) = bitcoin::Address::from_str(&address) { if !address.is_valid_for_network(Network::Bitcoin) { return Err(Error::InvalidNetwork); } @@ -109,16 +106,18 @@ pub fn get_address_info( let balance = address_data.balance(); - Ok(AddressInfo { - address: address.to_string(), - r#type: type_, - type_index, - utxo_count: address_data.utxo_count, - total_sent: address_data.sent, - total_received: address_data.received, - balance, - balance_usd: price.map(|p| p * Bitcoin::from(balance)), - estimated_total_invested: price.map(|_| address_data.realized_cap), - estimated_avg_entry_price: price.map(|_| address_data.realized_price()), - }) + todo!(); + + // Ok(Address { + // address: address.to_string(), + // r#type: type_, + // type_index, + // utxo_count: address_data.utxo_count, + // total_sent: address_data.sent, + // total_received: address_data.received, + // balance, + // balance_usd: price.map(|p| p * Bitcoin::from(balance)), + // estimated_total_invested: price.map(|_| address_data.realized_cap), + // estimated_avg_entry_price: price.map(|_| address_data.realized_price()), + // }) } diff --git a/crates/brk_interface/src/chain/transactions.rs b/crates/brk_interface/src/chain/transactions.rs index 88332eaed..cee48011d 100644 --- a/crates/brk_interface/src/chain/transactions.rs +++ b/crates/brk_interface/src/chain/transactions.rs @@ -7,15 +7,12 @@ use std::{ use bitcoin::{Transaction, consensus::Decodable}; use brk_error::{Error, Result}; use brk_parser::XORIndex; -use brk_structs::{TransactionInfo, Txid, TxidPath, TxidPrefix}; +use brk_structs::{Tx, Txid, TxidPath, TxidPrefix}; use vecdb::VecIterator; use crate::Interface; -pub fn get_transaction_info( - TxidPath { txid }: TxidPath, - interface: &Interface, -) -> Result { +pub fn get_transaction_info(TxidPath { txid }: TxidPath, interface: &Interface) -> Result { let Ok(txid) = bitcoin::Txid::from_str(&txid) else { return Err(Error::InvalidTxid); }; @@ -79,9 +76,11 @@ pub fn get_transaction_info( return Err(Error::Str("Failed decode the transaction")); }; - Ok(TransactionInfo { - txid, - index, - // tx - }) + todo!(); + + // Ok(TxInfo { + // txid, + // index, + // // tx + // }) } diff --git a/crates/brk_interface/src/lib.rs b/crates/brk_interface/src/lib.rs index de7139964..bab1058aa 100644 --- a/crates/brk_interface/src/lib.rs +++ b/crates/brk_interface/src/lib.rs @@ -7,26 +7,26 @@ use brk_error::Result; use brk_indexer::Indexer; use brk_parser::Parser; use brk_structs::{ - AddressInfo, AddressPath, Format, Height, Index, IndexInfo, MetricCount, MetricSearchQuery, - TransactionInfo, TxidPath, + Address, AddressStats, Format, Height, Index, IndexInfo, Limit, Metric, MetricCount, Tx, + TxidPath, }; use brk_traversable::TreeNode; use vecdb::{AnyCollectableVec, AnyStoredVec}; mod chain; mod deser; -mod metrics; +mod output; mod pagination; mod params; mod vecs; -pub use metrics::{Output, Value}; +pub use output::{Output, Value}; pub use pagination::{PaginatedIndexParam, PaginatedMetrics, PaginationParam}; pub use params::{Params, ParamsDeprec, ParamsOpt}; use vecs::Vecs; use crate::{ - chain::{get_address_info, get_transaction_info}, + chain::{get_address, get_transaction_info}, vecs::{IndexToVec, MetricToVec}, }; @@ -57,16 +57,16 @@ impl<'a> Interface<'a> { Height::from(self.indexer.vecs.height_to_blockhash.stamp()) } - pub fn get_address_info(&self, address: AddressPath) -> Result { - get_address_info(address, self) + pub fn get_address(&self, address: Address) -> Result { + get_address(address, self) } - pub fn get_transaction_info(&self, txid: TxidPath) -> Result { + pub fn get_transaction_info(&self, txid: TxidPath) -> Result { get_transaction_info(txid, self) } - pub fn match_metric(&self, query: MetricSearchQuery) -> Vec<&str> { - self.vecs.matches(query) + pub fn match_metric(&self, metric: &Metric, limit: Limit) -> Vec<&str> { + self.vecs.matches(metric, limit) } pub fn search_metric_with_index( diff --git a/crates/brk_interface/src/metrics/output.rs b/crates/brk_interface/src/output.rs similarity index 100% rename from crates/brk_interface/src/metrics/output.rs rename to crates/brk_interface/src/output.rs diff --git a/crates/brk_interface/src/params.rs b/crates/brk_interface/src/params.rs index 107a78cef..6d0f513d8 100644 --- a/crates/brk_interface/src/params.rs +++ b/crates/brk_interface/src/params.rs @@ -1,21 +1,18 @@ use std::ops::Deref; -use brk_structs::{Format, Index}; +use brk_structs::{Format, Index, Metric, Metrics}; +use derive_deref::Deref; use schemars::JsonSchema; use serde::Deserialize; -use crate::{ - deser::{de_unquote_i64, de_unquote_usize}, - metrics::MaybeMetrics, -}; +use crate::deser::{de_unquote_i64, de_unquote_usize}; #[derive(Debug, Deserialize, JsonSchema)] pub struct Params { /// Requested metrics #[serde(alias = "m")] - pub metrics: MaybeMetrics, + pub metrics: Metrics, - /// Requested index #[serde(alias = "i")] pub index: Index, @@ -30,11 +27,21 @@ impl Deref for Params { } } -impl From<((Index, String), ParamsOpt)> for Params { - fn from(((index, metric), rest): ((Index, String), ParamsOpt)) -> Self { +impl From<(Index, Metric, ParamsOpt)> for Params { + fn from((index, metric, rest): (Index, Metric, ParamsOpt)) -> Self { Self { index, - metrics: MaybeMetrics::from(metric), + metrics: Metrics::from(metric), + rest, + } + } +} + +impl From<(Index, Metrics, ParamsOpt)> for Params { + fn from((index, metrics, rest): (Index, Metrics, ParamsOpt)) -> Self { + Self { + index, + metrics, rest, } } @@ -105,7 +112,7 @@ pub struct ParamsDeprec { #[serde(alias = "i")] pub index: Index, #[serde(alias = "v")] - pub ids: MaybeMetrics, + pub ids: Metrics, #[serde(flatten)] pub rest: ParamsOpt, } diff --git a/crates/brk_interface/src/vecs.rs b/crates/brk_interface/src/vecs.rs index e46312f5a..8b898e68d 100644 --- a/crates/brk_interface/src/vecs.rs +++ b/crates/brk_interface/src/vecs.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use brk_computer::Computer; use brk_indexer::Indexer; -use brk_structs::{Index, IndexInfo, MetricSearchQuery}; +use brk_structs::{Index, IndexInfo, Limit, Metric}; use brk_traversable::{Traversable, TreeNode}; use derive_deref::{Deref, DerefMut}; use quickmatch::{QuickMatch, QuickMatchConfig}; @@ -172,9 +172,9 @@ impl<'a> Vecs<'a> { self.catalog.as_ref().unwrap() } - pub fn matches(&self, query: MetricSearchQuery) -> Vec<&'_ str> { + pub fn matches(&self, metric: &Metric, limit: Limit) -> Vec<&'_ str> { self.matcher() - .matches_with(&query.q, &QuickMatchConfig::new().with_limit(query.limit)) + .matches_with(metric, &QuickMatchConfig::new().with_limit(*limit)) } fn matcher(&self) -> &QuickMatch<'_> { diff --git a/crates/brk_mempool/README.md b/crates/brk_mempool/README.md deleted file mode 100644 index 3af9a1deb..000000000 --- a/crates/brk_mempool/README.md +++ /dev/null @@ -1 +0,0 @@ -# brk_mempool diff --git a/crates/brk_mempool/src/lib.rs b/crates/brk_mempool/src/lib.rs deleted file mode 100644 index a3aef3160..000000000 --- a/crates/brk_mempool/src/lib.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::{thread, time::Duration}; - -use bitcoin::{Transaction, consensus::encode}; -use bitcoincore_rpc::{Client, RpcApi}; -use log::error; -use parking_lot::{RwLock, RwLockReadGuard}; -use rustc_hash::FxHashMap; - -const MAX_FETCHES_PER_CYCLE: usize = 10_000; - -pub struct Mempool { - rpc: &'static Client, - txs: RwLock>, -} - -impl Mempool { - pub fn new(rpc: &'static Client) -> Self { - Self { - rpc, - txs: RwLock::new(FxHashMap::default()), - } - } - - pub fn get_txs(&self) -> RwLockReadGuard<'_, FxHashMap> { - self.txs.read() - } - - pub fn start(&self) { - loop { - if let Err(e) = self.update() { - error!("Error updating mempool: {}", e); - } - thread::sleep(Duration::from_secs(1)); - } - } - - fn update(&self) -> Result<(), Box> { - let current_txids = self.rpc.get_raw_mempool()?; - - let current_set: std::collections::HashSet = - current_txids.iter().map(|t| t.to_string()).collect(); - - // Fetch new transactions - let mut new_txs = FxHashMap::default(); - let mut fetched = 0; - - for txid in current_txids { - if fetched >= MAX_FETCHES_PER_CYCLE { - break; - } - - let txid_str = txid.to_string(); - if !self.txs.read().contains_key(&txid_str) - && let Ok(hex) = self.rpc.get_raw_transaction_hex(&txid, None) - { - let tx: Transaction = encode::deserialize_hex(&hex)?; - - new_txs.insert(txid_str, tx); - fetched += 1; - } - } - - { - let mut mempool = self.txs.write(); - mempool.retain(|txid, _| current_set.contains(txid)); - mempool.extend(new_txs); - } - - Ok(()) - } -} diff --git a/crates/brk_mempool/Cargo.toml b/crates/brk_monitor/Cargo.toml similarity index 66% rename from crates/brk_mempool/Cargo.toml rename to crates/brk_monitor/Cargo.toml index 6468eceb9..c54401df6 100644 --- a/crates/brk_mempool/Cargo.toml +++ b/crates/brk_monitor/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "brk_mempool" -description = "A Bitcoin mempool reader" +name = "brk_monitor" +description = "A Bitcoin mempool monitor with real-time synchronization" version.workspace = true edition.workspace = true license.workspace = true @@ -10,8 +10,8 @@ rust-version.workspace = true build = "build.rs" [dependencies] -bitcoin = { version = "0.32.7", features = ["serde"] } -bitcoincore-rpc = "0.19.0" +bitcoin = { workspace = true } +bitcoincore-rpc = { workspace = true } brk_structs = { workspace = true } log = { workspace = true } parking_lot = { workspace = true } diff --git a/crates/brk_monitor/README.md b/crates/brk_monitor/README.md new file mode 100644 index 000000000..3a1c8fb8d --- /dev/null +++ b/crates/brk_monitor/README.md @@ -0,0 +1,30 @@ +# brk_monitor + +A lightweight, thread-safe Rust library for maintaining a live, in-memory snapshot of the Bitcoin mempool. + +## Key Features + +- **Real-time synchronization**: Polls Bitcoin Core RPC every second to track mempool state +- **Thread-safe access**: Uses `RwLock` for concurrent reads with minimal contention +- **Efficient updates**: Only fetches new transactions, with configurable rate limiting (10,000 tx/cycle) +- **Zero-copy reads**: Exposes mempool via read guards for lock-free iteration +- **Optimized data structures**: Uses `FxHashMap` for fast lookups and minimal hashing overhead +- **Automatic cleanup**: Removes confirmed/dropped transactions on each update + +## Design Principles + +- **Minimal lock duration**: Lock held only during HashSet operations, never during I/O +- **Memory efficient**: Stores only missing txids during fetch phase +- **Simple API**: Just `new()`, `start()`, and `get_txs()` +- **Production-ready**: Error handling with logging, graceful degradation + +## Use Cases + +- Fee estimation and mempool analysis +- Transaction monitoring and alerts +- Block template prediction +- Network research and statistics + +## Description + +A clean, performant way to keep Bitcoin's mempool state available in your Rust application without repeatedly querying RPC. Perfect for applications that need frequent mempool access with low latency. diff --git a/crates/brk_mempool/build.rs b/crates/brk_monitor/build.rs similarity index 100% rename from crates/brk_mempool/build.rs rename to crates/brk_monitor/build.rs diff --git a/crates/brk_monitor/src/lib.rs b/crates/brk_monitor/src/lib.rs new file mode 100644 index 000000000..f4841e105 --- /dev/null +++ b/crates/brk_monitor/src/lib.rs @@ -0,0 +1,70 @@ +use std::{thread, time::Duration}; + +use bitcoin::{Transaction, Txid, consensus::encode}; +use bitcoincore_rpc::{Client, RpcApi}; +use log::error; +use parking_lot::{RwLock, RwLockReadGuard}; +use rustc_hash::{FxHashMap, FxHashSet}; + +const MAX_FETCHES_PER_CYCLE: usize = 10_000; + +pub struct Mempool { + rpc: &'static Client, + txs: RwLock>, +} + +impl Mempool { + pub fn new(rpc: &'static Client) -> Self { + Self { + rpc, + txs: RwLock::new(FxHashMap::default()), + } + } + + pub fn get_txs(&self) -> RwLockReadGuard<'_, FxHashMap> { + self.txs.read() + } + + pub fn start(&self) { + loop { + if let Err(e) = self.update() { + error!("Error updating mempool: {}", e); + } + thread::sleep(Duration::from_secs(1)); + } + } + + fn update(&self) -> Result<(), Box> { + let txids = self + .rpc + .get_raw_mempool()? + .into_iter() + .collect::>(); + + let missing_txids = { + let txs = self.txs.read(); + txids + .iter() + .filter(|txid| !txs.contains_key(*txid)) + .take(MAX_FETCHES_PER_CYCLE) + .collect::>() + }; + + let new_txs = missing_txids + .into_iter() + .filter_map(|txid| { + self.rpc + .get_raw_transaction_hex(txid, None) + .ok() + .and_then(|hex| encode::deserialize_hex(&hex).ok()) + .map(|tx| (*txid, tx)) + }) + .collect::>(); + + let mut txs = self.txs.write(); + txs.retain(|txid, _| txids.contains(txid)); + txs.extend(new_txs); + + Ok(()) + } +} diff --git a/crates/brk_mempool/src/main.rs b/crates/brk_monitor/src/main.rs similarity index 97% rename from crates/brk_mempool/src/main.rs rename to crates/brk_monitor/src/main.rs index a846831f6..29c489b86 100644 --- a/crates/brk_mempool/src/main.rs +++ b/crates/brk_monitor/src/main.rs @@ -1,6 +1,6 @@ use std::{path::Path, sync::Arc, thread, time::Duration}; -use brk_mempool::Mempool; +use brk_monitor::Mempool; fn main() { // Connect to Bitcoin Core diff --git a/crates/brk_server/src/api/addresses/mod.rs b/crates/brk_server/src/api/addresses/mod.rs index 0db3acb5c..dba744584 100644 --- a/crates/brk_server/src/api/addresses/mod.rs +++ b/crates/brk_server/src/api/addresses/mod.rs @@ -5,7 +5,7 @@ use axum::{ response::{Redirect, Response}, routing::get, }; -use brk_structs::{AddressInfo, AddressPath}; +use brk_structs::{Address, AddressStats}; use crate::{ VERSION, @@ -27,14 +27,14 @@ impl AddressRoutes for ApiRouter { "/api/address/{address}", get_with(async | headers: HeaderMap, - Path(address): Path, + Path(address): Path
, State(state): State | { let etag = format!("{VERSION}-{}", state.get_height()); if headers.has_etag(&etag) { return Response::new_not_modified(); } - match state.get_address_info(address).with_status() { + match state.get_address(address).with_status() { Ok(value) => Response::new_json(&value, &etag), Err((status, message)) => Response::new_json_with(status, &message, &etag) } @@ -42,7 +42,7 @@ impl AddressRoutes for ApiRouter { .addresses_tag() .summary("Address information") .description("Retrieve comprehensive information about a Bitcoin address including balance, transaction history, UTXOs, and estimated investment metrics. Supports all standard Bitcoin address types (P2PKH, P2SH, P2WPKH, P2WSH, P2TR, etc.).") - .ok_response::() + .ok_response::() .not_modified() .bad_request() .not_found() diff --git a/crates/brk_server/src/api/metrics/mod.rs b/crates/brk_server/src/api/metrics/mod.rs index 35665e48e..d51a1be42 100644 --- a/crates/brk_server/src/api/metrics/mod.rs +++ b/crates/brk_server/src/api/metrics/mod.rs @@ -6,7 +6,7 @@ use axum::{ routing::get, }; use brk_interface::{PaginatedMetrics, PaginationParam, Params, ParamsDeprec, ParamsOpt}; -use brk_structs::{Index, IndexInfo, MetricCount, MetricPath, MetricSearchQuery}; +use brk_structs::{Index, IndexInfo, Limit, Metric, MetricCount, Metrics}; use brk_traversable::TreeNode; use crate::{ @@ -114,18 +114,19 @@ impl ApiMetricsRoutes for ApiRouter { ), ) .api_route( - "/api/metrics/search", + "/api/metrics/search/{metric}", get_with( async | headers: HeaderMap, State(state): State, - Query(query): Query + Path(metric): Path, + Query(limit): Query | { let etag = VERSION; if headers.has_etag(etag) { return Response::new_not_modified(); } - Response::new_json(state.match_metric(query), etag) + Response::new_json(state.match_metric(&metric, limit), etag) }, |op| op .metrics_tag() @@ -141,7 +142,7 @@ impl ApiMetricsRoutes for ApiRouter { async | headers: HeaderMap, State(state): State, - Path(MetricPath { metric }): Path + Path(metric): Path | { let etag = VERSION; if headers.has_etag(etag) { @@ -150,10 +151,7 @@ impl ApiMetricsRoutes for ApiRouter { if let Some(indexes) = state.metric_to_indexes(metric.clone()) { return Response::new_json(indexes, etag) } - let value = if let Some(first) = state.match_metric(MetricSearchQuery { - q: metric.clone(), - limit: 1, - }).first() { + let value = if let Some(first) = state.match_metric(&metric, Limit::MIN).first() { format!("Could not find '{metric}', did you mean '{first}' ?") } else { format!("Could not find '{metric}'.") @@ -181,7 +179,7 @@ impl ApiMetricsRoutes for ApiRouter { async |uri: Uri, headers: HeaderMap, state: State, - Path((metric, index)): Path<(MetricPath, Index)>, + Path((metric, index)): Path<(Metric, Index)>, Query(params_opt): Query| -> Response { todo!(); @@ -232,7 +230,8 @@ impl ApiMetricsRoutes for ApiRouter { }; let params = Params::from(( - (index, split.collect::>().join(separator)), + index, + Metrics::from(split.collect::>().join(separator)), params_opt, )); data::handler(uri, headers, Query(params), state).await diff --git a/crates/brk_server/src/api/transactions/mod.rs b/crates/brk_server/src/api/transactions/mod.rs index 5c235b3dc..f242027dc 100644 --- a/crates/brk_server/src/api/transactions/mod.rs +++ b/crates/brk_server/src/api/transactions/mod.rs @@ -5,7 +5,7 @@ use axum::{ response::{Redirect, Response}, routing::get, }; -use brk_structs::{TransactionInfo, TxidPath}; +use brk_structs::{Tx, TxidPath}; use crate::{ VERSION, @@ -46,7 +46,7 @@ impl TxRoutes for ApiRouter { .description( "Retrieve complete transaction data by transaction ID (txid). Returns the full transaction details including inputs, outputs, and metadata. The transaction data is read directly from the blockchain data files.", ) - .ok_response::() + .ok_response::() .not_modified() .bad_request() .not_found() diff --git a/crates/brk_structs/Cargo.toml b/crates/brk_structs/Cargo.toml index 45c4bee51..64bb49de3 100644 --- a/crates/brk_structs/Cargo.toml +++ b/crates/brk_structs/Cargo.toml @@ -23,6 +23,7 @@ rapidhash = "4.1.0" ryu = "1.0.20" schemars = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } serde_bytes = { workspace = true } strum = { version = "0.27", features = ["derive"] } vecdb = { workspace = true } diff --git a/crates/brk_structs/src/address.rs b/crates/brk_structs/src/address.rs new file mode 100644 index 000000000..a6fbd7c6b --- /dev/null +++ b/crates/brk_structs/src/address.rs @@ -0,0 +1,112 @@ +use std::fmt; + +use bitcoin::{Network, ScriptBuf, opcodes, script::Builder}; +use brk_error::Error; +use derive_deref::Deref; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize, Serializer}; + +use crate::AddressBytes; + +use super::OutputType; + +#[derive(Debug, Deref, Deserialize, JsonSchema)] +pub struct Address { + /// Bitcoin address string + #[schemars(example = &"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f")] + pub address: String, +} + +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.address) + } +} + +impl From for Address { + fn from(address: String) -> Self { + Self { address } + } +} + +impl TryFrom for Address { + type Error = Error; + fn try_from(script: ScriptBuf) -> Result { + Ok(Self::from(bitcoin::Address::from_script( + &script, + Network::Bitcoin, + )?)) + } +} + +impl From for Address { + fn from(address: bitcoin::Address) -> Self { + Self { + address: address.to_string(), + } + } +} + +impl TryFrom<(&ScriptBuf, OutputType)> for Address { + type Error = Error; + fn try_from(tuple: (&ScriptBuf, OutputType)) -> Result { + Self::try_from(AddressBytes::try_from(tuple)?) + } +} + +impl TryFrom for Address { + type Error = Error; + fn try_from(bytes: AddressBytes) -> Result { + let address = match bytes { + AddressBytes::P2PK65(b) => Self::from(bytes_to_hex(&**b)), + AddressBytes::P2PK33(b) => Self::from(bytes_to_hex(&**b)), + AddressBytes::P2PKH(b) => Self::try_from( + Builder::new() + .push_opcode(opcodes::all::OP_DUP) + .push_opcode(opcodes::all::OP_HASH160) + .push_slice(**b) + .push_opcode(opcodes::all::OP_EQUALVERIFY) + .push_opcode(opcodes::all::OP_CHECKSIG) + .into_script(), + )?, + AddressBytes::P2SH(b) => Self::try_from( + Builder::new() + .push_opcode(opcodes::all::OP_HASH160) + .push_slice(**b) + .push_opcode(opcodes::all::OP_EQUAL) + .into_script(), + )?, + AddressBytes::P2WPKH(b) => { + Self::try_from(Builder::new().push_int(0).push_slice(**b).into_script())? + } + AddressBytes::P2WSH(b) => { + Self::try_from(Builder::new().push_int(0).push_slice(**b).into_script())? + } + AddressBytes::P2TR(b) => { + Self::try_from(Builder::new().push_int(1).push_slice(**b).into_script())? + } + AddressBytes::P2A(b) => { + Self::try_from(Builder::new().push_int(1).push_slice(**b).into_script())? + } + }; + Ok(address) + } +} + +fn bytes_to_hex(bytes: &[u8]) -> String { + let mut hex_string = String::with_capacity(bytes.len() * 2); + for byte in bytes { + use std::fmt::Write; + write!(&mut hex_string, "{:02x}", byte).unwrap(); + } + hex_string +} + +impl Serialize for Address { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_str(&self.address) + } +} diff --git a/crates/brk_structs/src/addressbytes.rs b/crates/brk_structs/src/addressbytes.rs index 26f9f80df..cffe4ca86 100644 --- a/crates/brk_structs/src/addressbytes.rs +++ b/crates/brk_structs/src/addressbytes.rs @@ -1,17 +1,10 @@ -use std::fmt; - -use bitcoin::{ - Address, Network, ScriptBuf, - hex::{Case, DisplayHex}, - opcodes, - script::Builder, -}; +use bitcoin::ScriptBuf; use brk_error::Error; -use derive_deref::{Deref, DerefMut}; -use serde::{Serialize, Serializer}; -use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; -use super::OutputType; +use super::{ + OutputType, P2ABytes, P2PK33Bytes, P2PK65Bytes, P2PKHBytes, P2SHBytes, P2TRBytes, P2WPKHBytes, + P2WSHBytes, +}; #[derive(Debug, PartialEq, Eq)] pub enum AddressBytes { @@ -25,21 +18,6 @@ pub enum AddressBytes { P2A(P2ABytes), } -impl fmt::Display for AddressBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&match self { - AddressBytes::P2PK65(bytes) => bytes.to_string(), - AddressBytes::P2PK33(bytes) => bytes.to_string(), - AddressBytes::P2PKH(bytes) => bytes.to_string(), - AddressBytes::P2SH(bytes) => bytes.to_string(), - AddressBytes::P2WPKH(bytes) => bytes.to_string(), - AddressBytes::P2WSH(bytes) => bytes.to_string(), - AddressBytes::P2TR(bytes) => bytes.to_string(), - AddressBytes::P2A(bytes) => bytes.to_string(), - }) - } -} - impl AddressBytes { pub fn as_slice(&self) -> &[u8] { match self { @@ -70,7 +48,7 @@ impl TryFrom<(&ScriptBuf, OutputType)> for AddressBytes { return Err(Error::WrongLength); } }; - Ok(Self::P2PK65(P2PK65Bytes(U8x65::from(bytes)))) + Ok(Self::P2PK65(P2PK65Bytes::from(bytes))) } OutputType::P2PK33 => { let bytes = script.as_bytes(); @@ -81,31 +59,31 @@ impl TryFrom<(&ScriptBuf, OutputType)> for AddressBytes { return Err(Error::WrongLength); } }; - Ok(Self::P2PK33(P2PK33Bytes(U8x33::from(bytes)))) + Ok(Self::P2PK33(P2PK33Bytes::from(bytes))) } OutputType::P2PKH => { let bytes = &script.as_bytes()[3..23]; - Ok(Self::P2PKH(P2PKHBytes(U8x20::from(bytes)))) + Ok(Self::P2PKH(P2PKHBytes::from(bytes))) } OutputType::P2SH => { let bytes = &script.as_bytes()[2..22]; - Ok(Self::P2SH(P2SHBytes(U8x20::from(bytes)))) + Ok(Self::P2SH(P2SHBytes::from(bytes))) } OutputType::P2WPKH => { let bytes = &script.as_bytes()[2..]; - Ok(Self::P2WPKH(P2WPKHBytes(U8x20::from(bytes)))) + Ok(Self::P2WPKH(P2WPKHBytes::from(bytes))) } OutputType::P2WSH => { let bytes = &script.as_bytes()[2..]; - Ok(Self::P2WSH(P2WSHBytes(U8x32::from(bytes)))) + Ok(Self::P2WSH(P2WSHBytes::from(bytes))) } OutputType::P2TR => { let bytes = &script.as_bytes()[2..]; - Ok(Self::P2TR(P2TRBytes(U8x32::from(bytes)))) + Ok(Self::P2TR(P2TRBytes::from(bytes))) } OutputType::P2A => { let bytes = &script.as_bytes()[2..]; - Ok(Self::P2A(P2ABytes(U8x2::from(bytes)))) + Ok(Self::P2A(P2ABytes::from(bytes))) } OutputType::P2MS => Err(Error::WrongAddressType), OutputType::Unknown => Err(Error::WrongAddressType), @@ -116,348 +94,50 @@ impl TryFrom<(&ScriptBuf, OutputType)> for AddressBytes { } } -#[derive(Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes)] -pub struct P2PK65Bytes(U8x65); - -impl fmt::Display for P2PK65Bytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.to_hex_string(Case::Lower)) - } -} - -impl Serialize for P2PK65Bytes { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(&self.to_string()) - } -} - impl From for AddressBytes { fn from(value: P2PK65Bytes) -> Self { Self::P2PK65(value) } } -#[derive(Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes)] -pub struct P2PK33Bytes(U8x33); - -impl fmt::Display for P2PK33Bytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.to_hex_string(Case::Lower)) - } -} - -impl Serialize for P2PK33Bytes { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(&self.to_string()) - } -} - impl From for AddressBytes { fn from(value: P2PK33Bytes) -> Self { Self::P2PK33(value) } } -#[derive(Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes)] -pub struct P2PKHBytes(U8x20); - -impl fmt::Display for P2PKHBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let script = Builder::new() - .push_opcode(opcodes::all::OP_DUP) - .push_opcode(opcodes::all::OP_HASH160) - .push_slice(*self.0) - .push_opcode(opcodes::all::OP_EQUALVERIFY) - .push_opcode(opcodes::all::OP_CHECKSIG) - .into_script(); - let address = Address::from_script(&script, Network::Bitcoin).unwrap(); - write!(f, "{address}") - } -} - -impl Serialize for P2PKHBytes { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(&self.to_string()) - } -} - impl From for AddressBytes { fn from(value: P2PKHBytes) -> Self { Self::P2PKH(value) } } -#[derive(Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes)] -pub struct P2SHBytes(U8x20); - -impl fmt::Display for P2SHBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let script = Builder::new() - .push_opcode(opcodes::all::OP_HASH160) - .push_slice(*self.0) - .push_opcode(opcodes::all::OP_EQUAL) - .into_script(); - let address = Address::from_script(&script, Network::Bitcoin).unwrap(); - write!(f, "{address}") - } -} - -impl Serialize for P2SHBytes { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(&self.to_string()) - } -} - impl From for AddressBytes { fn from(value: P2SHBytes) -> Self { Self::P2SH(value) } } -#[derive(Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes)] -pub struct P2WPKHBytes(U8x20); - -impl fmt::Display for P2WPKHBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let script = Builder::new().push_int(0).push_slice(*self.0).into_script(); - let address = Address::from_script(&script, Network::Bitcoin).unwrap(); - write!(f, "{address}") - } -} - -impl Serialize for P2WPKHBytes { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(&self.to_string()) - } -} - impl From for AddressBytes { fn from(value: P2WPKHBytes) -> Self { Self::P2WPKH(value) } } -#[derive(Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes)] -pub struct P2WSHBytes(U8x32); - -impl fmt::Display for P2WSHBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let script = Builder::new().push_int(0).push_slice(*self.0).into_script(); - let address = Address::from_script(&script, Network::Bitcoin).unwrap(); - write!(f, "{address}") - } -} - -impl Serialize for P2WSHBytes { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(&self.to_string()) - } -} - impl From for AddressBytes { fn from(value: P2WSHBytes) -> Self { Self::P2WSH(value) } } -#[derive(Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes)] -pub struct P2TRBytes(U8x32); - -impl fmt::Display for P2TRBytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let script = Builder::new().push_int(1).push_slice(*self.0).into_script(); - let address = Address::from_script(&script, Network::Bitcoin).unwrap(); - write!(f, "{address}") - } -} - -impl Serialize for P2TRBytes { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(&self.to_string()) - } -} - impl From for AddressBytes { fn from(value: P2TRBytes) -> Self { Self::P2TR(value) } } -#[derive(Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes)] -pub struct P2ABytes(U8x2); - -impl fmt::Display for P2ABytes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let script = Builder::new().push_int(1).push_slice(*self.0).into_script(); - let address = Address::from_script(&script, Network::Bitcoin).unwrap(); - write!(f, "{address}") - } -} - -impl Serialize for P2ABytes { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.collect_str(&self.to_string()) - } -} - impl From for AddressBytes { fn from(value: P2ABytes) -> Self { Self::P2A(value) } } - -#[derive( - Debug, - Clone, - Deref, - DerefMut, - PartialEq, - Eq, - Immutable, - IntoBytes, - KnownLayout, - FromBytes, - Serialize, -)] -pub struct U8x2([u8; 2]); -impl From<&[u8]> for U8x2 { - fn from(slice: &[u8]) -> Self { - let mut arr = [0; 2]; - arr.copy_from_slice(slice); - Self(arr) - } -} - -#[derive( - Debug, - Clone, - Deref, - DerefMut, - PartialEq, - Eq, - Immutable, - IntoBytes, - KnownLayout, - FromBytes, - Serialize, -)] -pub struct U8x20([u8; 20]); -impl From<&[u8]> for U8x20 { - fn from(slice: &[u8]) -> Self { - let mut arr = [0; 20]; - arr.copy_from_slice(slice); - Self(arr) - } -} - -#[derive( - Debug, - Clone, - Deref, - DerefMut, - PartialEq, - Eq, - Immutable, - IntoBytes, - KnownLayout, - FromBytes, - Serialize, -)] -pub struct U8x32([u8; 32]); -impl From<&[u8]> for U8x32 { - fn from(slice: &[u8]) -> Self { - let mut arr = [0; 32]; - arr.copy_from_slice(slice); - Self(arr) - } -} - -#[derive( - Debug, - Clone, - Deref, - DerefMut, - PartialEq, - Eq, - Immutable, - IntoBytes, - KnownLayout, - FromBytes, - Serialize, -)] -pub struct U8x33(#[serde(with = "serde_bytes")] [u8; 33]); -impl From<&[u8]> for U8x33 { - fn from(slice: &[u8]) -> Self { - let mut arr = [0; 33]; - arr.copy_from_slice(slice); - Self(arr) - } -} - -#[derive( - Debug, - Clone, - Deref, - DerefMut, - PartialEq, - Eq, - Immutable, - IntoBytes, - KnownLayout, - FromBytes, - Serialize, -)] -pub struct U8x64(#[serde(with = "serde_bytes")] [u8; 64]); -impl From<&[u8]> for U8x64 { - fn from(slice: &[u8]) -> Self { - let mut arr = [0; 64]; - arr.copy_from_slice(slice); - Self(arr) - } -} - -#[derive( - Debug, - Clone, - Deref, - DerefMut, - PartialEq, - Eq, - Immutable, - IntoBytes, - KnownLayout, - FromBytes, - Serialize, -)] -pub struct U8x65(#[serde(with = "serde_bytes")] [u8; 65]); -impl From<&[u8]> for U8x65 { - fn from(slice: &[u8]) -> Self { - let mut arr = [0; 65]; - arr.copy_from_slice(slice); - Self(arr) - } -} diff --git a/crates/brk_structs/src/addresschainstats.rs b/crates/brk_structs/src/addresschainstats.rs new file mode 100644 index 000000000..8f7027957 --- /dev/null +++ b/crates/brk_structs/src/addresschainstats.rs @@ -0,0 +1,33 @@ +use crate::{Sats, TypeIndex}; +use schemars::JsonSchema; +use serde::Serialize; + +/// Address statistics on the blockchain (confirmed transactions only) +/// +/// Based on mempool.space's format with type_index extension. +#[derive(Debug, Serialize, JsonSchema)] +pub struct AddressChainStats { + /// Total number of transaction outputs that funded this address + #[schemars(example = 5)] + pub funded_txo_count: u64, + + /// Total amount in satoshis received by this address across all funded outputs + #[schemars(example = Sats::new(15007599040))] + pub funded_txo_sum: Sats, + + /// Total number of transaction outputs spent from this address + #[schemars(example = 5)] + pub spent_txo_count: u64, + + /// Total amount in satoshis spent from this address + #[schemars(example = Sats::new(15007599040))] + pub spent_txo_sum: Sats, + + /// Total number of confirmed transactions involving this address + #[schemars(example = 10)] + pub tx_count: Option, + + /// Index of this address within its type on the blockchain + #[schemars(example = TypeIndex::new(0))] + pub type_index: TypeIndex, +} diff --git a/crates/brk_structs/src/addressinfo.rs b/crates/brk_structs/src/addressinfo.rs deleted file mode 100644 index f12e7c1c8..000000000 --- a/crates/brk_structs/src/addressinfo.rs +++ /dev/null @@ -1,69 +0,0 @@ -use schemars::JsonSchema; -use serde::Serialize; - -use crate::{Dollars, OutputType, Sats, TypeIndex}; - -#[derive(Debug, Serialize, JsonSchema)] -/// Address information -pub struct AddressInfo { - /// Bitcoin address string - #[schemars(example = &"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f")] - pub address: String, - - #[schemars(example = OutputType::P2PK65)] - pub r#type: OutputType, - - #[schemars(example = TypeIndex::new(0))] - pub type_index: TypeIndex, - - /// Total satoshis ever sent from this address - #[schemars(example = Sats::new(0))] - pub total_sent: Sats, - - /// Total satoshis ever received by this address - #[schemars(example = Sats::new(5001008380))] - pub total_received: Sats, - - /// Number of unspent transaction outputs (UTXOs) - #[schemars(example = 10)] - pub utxo_count: u32, - - /// Current spendable balance in satoshis (total_received - total_sent) - #[schemars(example = Sats::new(5001008380))] - pub balance: Sats, - - /// Current balance value in USD at current market price - #[schemars(example = Some(Dollars::mint(6_157_891.64)))] - pub balance_usd: Option, - - /// Estimated total USD value at time of deposit for coins currently in this address (not including coins that were later sent out). Not suitable for tax calculations - #[schemars(example = Some(Dollars::mint(6.2)))] - pub estimated_total_invested: Option, - - /// Estimated average BTC price at time of deposit for coins currently in this address (USD). Not suitable for tax calculations - #[schemars(example = Some(Dollars::mint(0.12)))] - pub estimated_avg_entry_price: Option, - // - // Transaction count? - // First/last activity timestamps? - // Realized/unrealized gains? - // Current value (balance × current price)? - // "address": address, - // "type": output_type, - // "index": addri, - // "chain_stats": { - // "funded_txo_count": null, - // "funded_txo_sum": addr_data.received, - // "spent_txo_count": null, - // "spent_txo_sum": addr_data.sent, - // "utxo_count": addr_data.utxos, - // "balance": amount, - // "balance_usd": price.map_or(Value::new(), |p| { - // Value::from(Number::from_f64(*(p * Bitcoin::from(amount))).unwrap()) - // }), - // "realized_value": addr_data.realized_cap, - // "tx_count": null, - // "avg_cost_basis": addr_data.realized_price() - // }, - // "mempool_stats": null -} diff --git a/crates/brk_structs/src/addressmempoolstats.rs b/crates/brk_structs/src/addressmempoolstats.rs new file mode 100644 index 000000000..48d3b4ead --- /dev/null +++ b/crates/brk_structs/src/addressmempoolstats.rs @@ -0,0 +1,31 @@ +use crate::Sats; +use schemars::JsonSchema; +use serde::Serialize; + +/// +/// Address statistics in the mempool (unconfirmed transactions only) +/// +/// Based on mempool.space's format. +/// +#[derive(Debug, Serialize, JsonSchema)] +pub struct AddressMempoolStats { + /// Number of unconfirmed transaction outputs funding this address + #[schemars(example = 0)] + pub funded_txo_count: u32, + + /// Total amount in satoshis being received in unconfirmed transactions + #[schemars(example = Sats::new(0))] + pub funded_txo_sum: Sats, + + /// Number of unconfirmed transaction inputs spending from this address + #[schemars(example = 0)] + pub spent_txo_count: u32, + + /// Total amount in satoshis being spent in unconfirmed transactions + #[schemars(example = Sats::new(0))] + pub spent_txo_sum: Sats, + + /// Number of unconfirmed transactions involving this address + #[schemars(example = 0)] + pub tx_count: u32, +} diff --git a/crates/brk_structs/src/addresspath.rs b/crates/brk_structs/src/addresspath.rs deleted file mode 100644 index 92cde65f2..000000000 --- a/crates/brk_structs/src/addresspath.rs +++ /dev/null @@ -1,9 +0,0 @@ -use schemars::JsonSchema; -use serde::Deserialize; - -#[derive(Deserialize, JsonSchema)] -pub struct AddressPath { - /// Bitcoin address string - #[schemars(example = &"04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f")] - pub address: String, -} diff --git a/crates/brk_structs/src/addressstats.rs b/crates/brk_structs/src/addressstats.rs new file mode 100644 index 000000000..9c1f81ae4 --- /dev/null +++ b/crates/brk_structs/src/addressstats.rs @@ -0,0 +1,19 @@ +use crate::{AddressChainStats, AddressMempoolStats}; +use schemars::JsonSchema; +use serde::Serialize; + +#[derive(Debug, Serialize, JsonSchema)] +/// Address information compatible with mempool.space API format +pub struct AddressStats { + /// Bitcoin address string + #[schemars( + example = "04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f" + )] + pub address: String, + + /// Statistics for confirmed transactions on the blockchain + pub chain_stats: AddressChainStats, + + /// Statistics for unconfirmed transactions in the mempool + pub mempool_stats: AddressMempoolStats, +} diff --git a/crates/brk_structs/src/blockhash.rs b/crates/brk_structs/src/blockhash.rs index 02f90f1d8..da0457151 100644 --- a/crates/brk_structs/src/blockhash.rs +++ b/crates/brk_structs/src/blockhash.rs @@ -3,12 +3,16 @@ use std::{fmt, mem}; use bitcoin::hashes::Hash; use bitcoincore_rpc::{Client, RpcApi}; use derive_deref::Deref; +use schemars::JsonSchema; use serde::{Serialize, Serializer}; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; use super::Height; -#[derive(Debug, Deref, Clone, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes)] +/// Block hash +#[derive( + Debug, Deref, Clone, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes, JsonSchema, +)] pub struct BlockHash([u8; 32]); impl From for BlockHash { diff --git a/crates/brk_structs/src/bytes.rs b/crates/brk_structs/src/bytes.rs new file mode 100644 index 000000000..717a2cb59 --- /dev/null +++ b/crates/brk_structs/src/bytes.rs @@ -0,0 +1,113 @@ +use derive_deref::{Deref, DerefMut}; +use serde::Serialize; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +#[derive( + Debug, + Clone, + Deref, + DerefMut, + PartialEq, + Eq, + Immutable, + IntoBytes, + KnownLayout, + FromBytes, + Serialize, +)] +pub struct U8x2([u8; 2]); +impl From<&[u8]> for U8x2 { + fn from(slice: &[u8]) -> Self { + let mut arr = [0; 2]; + arr.copy_from_slice(slice); + Self(arr) + } +} + +#[derive( + Debug, + Clone, + Deref, + DerefMut, + PartialEq, + Eq, + Immutable, + IntoBytes, + KnownLayout, + FromBytes, + Serialize, +)] +pub struct U8x20([u8; 20]); +impl From<&[u8]> for U8x20 { + fn from(slice: &[u8]) -> Self { + let mut arr = [0; 20]; + arr.copy_from_slice(slice); + Self(arr) + } +} + +#[derive( + Debug, + Clone, + Deref, + DerefMut, + PartialEq, + Eq, + Immutable, + IntoBytes, + KnownLayout, + FromBytes, + Serialize, +)] +pub struct U8x32([u8; 32]); +impl From<&[u8]> for U8x32 { + fn from(slice: &[u8]) -> Self { + let mut arr = [0; 32]; + arr.copy_from_slice(slice); + Self(arr) + } +} + +#[derive( + Debug, + Clone, + Deref, + DerefMut, + PartialEq, + Eq, + Immutable, + IntoBytes, + KnownLayout, + FromBytes, + Serialize, +)] +pub struct U8x33(#[serde(with = "serde_bytes")] [u8; 33]); +impl From<&[u8]> for U8x33 { + fn from(slice: &[u8]) -> Self { + let mut arr = [0; 33]; + arr.copy_from_slice(slice); + Self(arr) + } +} + +#[derive( + Debug, + Clone, + Deref, + DerefMut, + PartialEq, + Eq, + Immutable, + IntoBytes, + KnownLayout, + FromBytes, + Serialize, +)] +pub struct U8x65(#[serde(with = "serde_bytes")] [u8; 65]); +impl From<&[u8]> for U8x65 { + fn from(slice: &[u8]) -> Self { + let mut arr = [0; 65]; + arr.copy_from_slice(slice); + Self(arr) + } +} diff --git a/crates/brk_structs/src/height.rs b/crates/brk_structs/src/height.rs index 95969215c..b39f3941f 100644 --- a/crates/brk_structs/src/height.rs +++ b/crates/brk_structs/src/height.rs @@ -7,6 +7,7 @@ use allocative::Allocative; use bitcoincore_rpc::{Client, RpcApi}; use byteview::ByteView; use derive_deref::Deref; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use vecdb::{CheckedSub, PrintableIndex, Stamp, StoredCompressed}; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; @@ -15,6 +16,7 @@ use crate::{BLOCKS_PER_DIFF_EPOCHS, BLOCKS_PER_HALVING, copy_first_4bytes}; use super::StoredU64; +/// Block height #[derive( Debug, Clone, @@ -33,6 +35,7 @@ use super::StoredU64; KnownLayout, StoredCompressed, Allocative, + JsonSchema, )] pub struct Height(u32); diff --git a/crates/brk_structs/src/index.rs b/crates/brk_structs/src/index.rs index f341abf3d..5b8814885 100644 --- a/crates/brk_structs/src/index.rs +++ b/crates/brk_structs/src/index.rs @@ -13,10 +13,10 @@ use super::{ SemesterIndex, TxIndex, UnknownOutputIndex, WeekIndex, YearIndex, }; +/// Aggregation dimension for querying Bitcoin blockchain data #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, JsonSchema)] #[serde(rename_all = "lowercase")] #[schemars(example = Index::DateIndex)] -/// Aggregation dimension for querying Bitcoin blockchain data pub enum Index { /// Date/day index DateIndex, diff --git a/crates/brk_structs/src/lib.rs b/crates/brk_structs/src/lib.rs index 93bcf3537..f94f31174 100644 --- a/crates/brk_structs/src/lib.rs +++ b/crates/brk_structs/src/lib.rs @@ -4,10 +4,12 @@ pub use vecdb::{CheckedSub, Exit, PrintableIndex, Version}; use brk_error::{Error, Result}; +mod address; mod addressbytes; mod addressbyteshash; -mod addressinfo; -mod addresspath; +mod addresschainstats; +mod addressmempoolstats; +mod addressstats; mod anyaddressindex; mod bitcoin; mod blkmetadata; @@ -15,6 +17,7 @@ mod blkposition; mod block; mod blockhash; mod blockhashprefix; +mod bytes; mod cents; mod date; mod dateindex; @@ -32,25 +35,34 @@ mod height; mod index; mod indexinfo; mod inputindex; +mod limit; mod loadedaddressdata; mod loadedaddressindex; +mod metric; mod metriccount; -mod metricpath; -mod metricsearchquery; +mod metrics; mod monthindex; mod ohlc; mod opreturnindex; mod outputindex; mod outputtype; mod p2aaddressindex; +mod p2abytes; mod p2msoutputindex; mod p2pk33addressindex; +mod p2pk33bytes; mod p2pk65addressindex; +mod p2pk65bytes; mod p2pkhaddressindex; +mod p2pkhbytes; mod p2shaddressindex; +mod p2shbytes; mod p2traddressindex; +mod p2trbytes; mod p2wpkhaddressindex; +mod p2wpkhbytes; mod p2wshaddressindex; +mod p2wshbytes; mod pool; mod poolid; mod pools; @@ -69,11 +81,15 @@ mod stored_u64; mod stored_u8; mod timestamp; mod treenode; +mod tx; mod txid; mod txidpath; mod txidprefix; mod txindex; -mod txinfo; +mod txinput; +mod txoutput; +mod txprevout; +mod txstatus; mod txversion; mod typeindex; mod typeindex_with_outputindex; @@ -85,10 +101,12 @@ mod weekindex; mod weight; mod yearindex; +pub use address::*; pub use addressbytes::*; pub use addressbyteshash::*; -pub use addressinfo::*; -pub use addresspath::*; +pub use addresschainstats::*; +pub use addressmempoolstats::*; +pub use addressstats::*; pub use anyaddressindex::*; pub use bitcoin::*; pub use blkmetadata::*; @@ -96,6 +114,7 @@ pub use blkposition::*; pub use block::*; pub use blockhash::*; pub use blockhashprefix::*; +pub use bytes::*; pub use cents::*; pub use date::*; pub use dateindex::*; @@ -113,25 +132,34 @@ pub use height::*; pub use index::*; pub use indexinfo::*; pub use inputindex::*; +pub use limit::*; pub use loadedaddressdata::*; pub use loadedaddressindex::*; +pub use metric::*; pub use metriccount::*; -pub use metricpath::*; -pub use metricsearchquery::*; +pub use metrics::*; pub use monthindex::*; pub use ohlc::*; pub use opreturnindex::*; pub use outputindex::*; pub use outputtype::*; pub use p2aaddressindex::*; +pub use p2abytes::*; pub use p2msoutputindex::*; pub use p2pk33addressindex::*; +pub use p2pk33bytes::*; pub use p2pk65addressindex::*; +pub use p2pk65bytes::*; pub use p2pkhaddressindex::*; +pub use p2pkhbytes::*; pub use p2shaddressindex::*; +pub use p2shbytes::*; pub use p2traddressindex::*; +pub use p2trbytes::*; pub use p2wpkhaddressindex::*; +pub use p2wpkhbytes::*; pub use p2wshaddressindex::*; +pub use p2wshbytes::*; pub use pool::*; pub use poolid::*; pub use pools::*; @@ -150,11 +178,15 @@ pub use stored_u32::*; pub use stored_u64::*; pub use timestamp::*; pub use treenode::*; +pub use tx::*; pub use txid::*; pub use txidpath::*; pub use txidprefix::*; pub use txindex::*; -pub use txinfo::*; +pub use txinput::*; +pub use txoutput::*; +pub use txprevout::*; +pub use txstatus::*; pub use txversion::*; pub use typeindex::*; pub use typeindex_with_outputindex::*; diff --git a/crates/brk_structs/src/metricsearchquery.rs b/crates/brk_structs/src/limit.rs similarity index 52% rename from crates/brk_structs/src/metricsearchquery.rs rename to crates/brk_structs/src/limit.rs index 2e3d00d3c..5dd7012ed 100644 --- a/crates/brk_structs/src/metricsearchquery.rs +++ b/crates/brk_structs/src/limit.rs @@ -1,13 +1,9 @@ +use derive_deref::Deref; use schemars::JsonSchema; use serde::Deserialize; -#[derive(Debug, Deserialize, JsonSchema)] -/// Search query parameters for finding metrics by name -pub struct MetricSearchQuery { - /// Search query string. Supports fuzzy matching, partial matches, and typos. - #[schemars(example = &"price", example = &"low", example = &"sth", example = &"realized", example = &"pric")] - pub q: String, - +#[derive(Debug, Deref, Deserialize, JsonSchema)] +pub struct Limit { /// Maximum number of results to return. Defaults to 100 if not specified. #[serde(default = "default_search_limit")] #[schemars( @@ -18,7 +14,11 @@ pub struct MetricSearchQuery { example = "10000", example = "100000" )] - pub limit: usize, + limit: usize, +} + +impl Limit { + pub const MIN: Self = Self { limit: 1 }; } fn default_search_limit() -> usize { diff --git a/crates/brk_structs/src/metric.rs b/crates/brk_structs/src/metric.rs new file mode 100644 index 000000000..5abdf1b20 --- /dev/null +++ b/crates/brk_structs/src/metric.rs @@ -0,0 +1,32 @@ +use std::fmt::Display; + +use derive_deref::Deref; +use schemars::JsonSchema; +use serde::Deserialize; + +#[derive(Debug, Deref, Deserialize, JsonSchema)] +pub struct Metric { + /// Metric name + #[schemars(example = &"price_close", example = &"market_cap", example = &"realized_price")] + metric: String, +} + +impl From for Metric { + fn from(metric: String) -> Self { + Self { metric } + } +} + +impl From<&str> for Metric { + fn from(metric: &str) -> Self { + Self { + metric: metric.to_string(), + } + } +} + +impl Display for Metric { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.metric) + } +} diff --git a/crates/brk_structs/src/metricpath.rs b/crates/brk_structs/src/metricpath.rs deleted file mode 100644 index 0b787489d..000000000 --- a/crates/brk_structs/src/metricpath.rs +++ /dev/null @@ -1,9 +0,0 @@ -use schemars::JsonSchema; -use serde::Deserialize; - -#[derive(Deserialize, JsonSchema)] -pub struct MetricPath { - /// Metric name - #[schemars(example = &"price_close", example = &"market_cap", example = &"realized_price")] - pub metric: String, -} diff --git a/crates/brk_interface/src/metrics/mod.rs b/crates/brk_structs/src/metrics.rs similarity index 52% rename from crates/brk_interface/src/metrics/mod.rs rename to crates/brk_structs/src/metrics.rs index 6b366286b..cf2326daa 100644 --- a/crates/brk_interface/src/metrics/mod.rs +++ b/crates/brk_structs/src/metrics.rs @@ -4,34 +4,43 @@ use derive_deref::Deref; use schemars::JsonSchema; use serde::Deserialize; -mod output; - -pub use output::*; +use super::Metric; #[derive(Debug, Deref, JsonSchema)] -pub struct MaybeMetrics(Vec); +pub struct Metrics { + /// A list of metrics + metrics: Vec, +} const MAX_VECS: usize = 32; const MAX_STRING_SIZE: usize = 64 * MAX_VECS; -impl From for MaybeMetrics { +impl From for Metrics { + fn from(metric: Metric) -> Self { + Self { + metrics: vec![metric], + } + } +} + +impl From for Metrics { fn from(value: String) -> Self { - Self(vec![value.replace("-", "_").to_lowercase()]) + Self::from(Metric::from(value.replace("-", "_").to_lowercase())) } } -impl<'a> From> for MaybeMetrics { +impl<'a> From> for Metrics { fn from(value: Vec<&'a str>) -> Self { - Self( - value + Self { + metrics: value .iter() - .map(|s| s.replace("-", "_").to_lowercase()) + .map(|s| Metric::from(s.replace("-", "_").to_lowercase())) .collect::>(), - ) + } } } -impl<'de> Deserialize<'de> for MaybeMetrics { +impl<'de> Deserialize<'de> for Metrics { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -40,17 +49,23 @@ impl<'de> Deserialize<'de> for MaybeMetrics { if let Some(str) = value.as_str() { if str.len() <= MAX_STRING_SIZE { - Ok(MaybeMetrics(sanitize_metrics( - str.split(",").map(|s| s.to_string()), - ))) + Ok(Self { + metrics: sanitize(str.split(",").map(|s| s.to_string())) + .into_iter() + .map(Metric::from) + .collect(), + }) } else { Err(serde::de::Error::custom("Given parameter is too long")) } } else if let Some(vec) = value.as_array() { if vec.len() <= MAX_VECS { - Ok(MaybeMetrics(sanitize_metrics( - vec.iter().map(|s| s.as_str().unwrap().to_string()), - ))) + Ok(Self { + metrics: sanitize(vec.iter().map(|s| s.as_str().unwrap().to_string())) + .into_iter() + .map(Metric::from) + .collect(), + }) } else { Err(serde::de::Error::custom("Given parameter is too long")) } @@ -60,22 +75,27 @@ impl<'de> Deserialize<'de> for MaybeMetrics { } } -impl fmt::Display for MaybeMetrics { +impl fmt::Display for Metrics { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let s = self.0.join(","); + let s = self + .metrics + .iter() + .map(|m| m.to_string()) + .collect::>() + .join(","); write!(f, "{s}") } } -fn sanitize_metrics(raw_ids: impl Iterator) -> Vec { - let mut results = Vec::new(); - raw_ids.for_each(|s| { +fn sanitize(dirty: impl Iterator) -> Vec { + let mut clean = Vec::new(); + dirty.for_each(|s| { let mut current = String::new(); for c in s.to_lowercase().chars() { match c { ' ' | ',' | '+' => { if !current.is_empty() { - results.push(std::mem::take(&mut current)); + clean.push(std::mem::take(&mut current)); } } '-' => current.push('_'), @@ -84,8 +104,8 @@ fn sanitize_metrics(raw_ids: impl Iterator) -> Vec { } } if !current.is_empty() { - results.push(current); + clean.push(current); } }); - results + clean } diff --git a/crates/brk_structs/src/p2abytes.rs b/crates/brk_structs/src/p2abytes.rs new file mode 100644 index 000000000..6e23d20aa --- /dev/null +++ b/crates/brk_structs/src/p2abytes.rs @@ -0,0 +1,30 @@ +use std::fmt; + +use derive_deref::Deref; +use serde::Serialize; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +use crate::U8x2; + +#[derive( + Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes, Serialize, +)] +pub struct P2ABytes(U8x2); + +impl From<&[u8]> for P2ABytes { + fn from(value: &[u8]) -> Self { + Self(U8x2::from(value)) + } +} + +impl From for P2ABytes { + fn from(value: U8x2) -> Self { + Self(value) + } +} + +impl fmt::Display for P2ABytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} diff --git a/crates/brk_structs/src/p2pk33bytes.rs b/crates/brk_structs/src/p2pk33bytes.rs new file mode 100644 index 000000000..939e6de5b --- /dev/null +++ b/crates/brk_structs/src/p2pk33bytes.rs @@ -0,0 +1,30 @@ +use std::fmt; + +use derive_deref::Deref; +use serde::Serialize; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +use crate::U8x33; + +#[derive( + Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes, Serialize, +)] +pub struct P2PK33Bytes(U8x33); + +impl From<&[u8]> for P2PK33Bytes { + fn from(value: &[u8]) -> Self { + Self(U8x33::from(value)) + } +} + +impl From for P2PK33Bytes { + fn from(value: U8x33) -> Self { + Self(value) + } +} + +impl fmt::Display for P2PK33Bytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} diff --git a/crates/brk_structs/src/p2pk65bytes.rs b/crates/brk_structs/src/p2pk65bytes.rs new file mode 100644 index 000000000..cf064f3c8 --- /dev/null +++ b/crates/brk_structs/src/p2pk65bytes.rs @@ -0,0 +1,30 @@ +use std::fmt; + +use derive_deref::Deref; +use serde::Serialize; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +use crate::U8x65; + +#[derive( + Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes, Serialize, +)] +pub struct P2PK65Bytes(U8x65); + +impl From<&[u8]> for P2PK65Bytes { + fn from(value: &[u8]) -> Self { + Self(U8x65::from(value)) + } +} + +impl From for P2PK65Bytes { + fn from(value: U8x65) -> Self { + Self(value) + } +} + +impl fmt::Display for P2PK65Bytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} diff --git a/crates/brk_structs/src/p2pkhbytes.rs b/crates/brk_structs/src/p2pkhbytes.rs new file mode 100644 index 000000000..8e69d8b2f --- /dev/null +++ b/crates/brk_structs/src/p2pkhbytes.rs @@ -0,0 +1,30 @@ +use std::fmt; + +use derive_deref::Deref; +use serde::Serialize; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +use crate::U8x20; + +#[derive( + Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes, Serialize, +)] +pub struct P2PKHBytes(U8x20); + +impl From<&[u8]> for P2PKHBytes { + fn from(value: &[u8]) -> Self { + Self(U8x20::from(value)) + } +} + +impl From for P2PKHBytes { + fn from(value: U8x20) -> Self { + Self(value) + } +} + +impl fmt::Display for P2PKHBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} diff --git a/crates/brk_structs/src/p2shbytes.rs b/crates/brk_structs/src/p2shbytes.rs new file mode 100644 index 000000000..7c5ebb75a --- /dev/null +++ b/crates/brk_structs/src/p2shbytes.rs @@ -0,0 +1,30 @@ +use std::fmt; + +use derive_deref::Deref; +use serde::Serialize; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +use crate::U8x20; + +#[derive( + Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes, Serialize, +)] +pub struct P2SHBytes(U8x20); + +impl From<&[u8]> for P2SHBytes { + fn from(value: &[u8]) -> Self { + Self(U8x20::from(value)) + } +} + +impl From for P2SHBytes { + fn from(value: U8x20) -> Self { + Self(value) + } +} + +impl fmt::Display for P2SHBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} diff --git a/crates/brk_structs/src/p2trbytes.rs b/crates/brk_structs/src/p2trbytes.rs new file mode 100644 index 000000000..108140e2d --- /dev/null +++ b/crates/brk_structs/src/p2trbytes.rs @@ -0,0 +1,30 @@ +use std::fmt; + +use derive_deref::Deref; +use serde::Serialize; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +use crate::U8x32; + +#[derive( + Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes, Serialize, +)] +pub struct P2TRBytes(U8x32); + +impl From<&[u8]> for P2TRBytes { + fn from(value: &[u8]) -> Self { + Self(U8x32::from(value)) + } +} + +impl From for P2TRBytes { + fn from(value: U8x32) -> Self { + Self(value) + } +} + +impl fmt::Display for P2TRBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} diff --git a/crates/brk_structs/src/p2wpkhbytes.rs b/crates/brk_structs/src/p2wpkhbytes.rs new file mode 100644 index 000000000..91d3de6fa --- /dev/null +++ b/crates/brk_structs/src/p2wpkhbytes.rs @@ -0,0 +1,30 @@ +use std::fmt; + +use derive_deref::Deref; +use serde::Serialize; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +use crate::U8x20; + +#[derive( + Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes, Serialize, +)] +pub struct P2WPKHBytes(U8x20); + +impl From<&[u8]> for P2WPKHBytes { + fn from(value: &[u8]) -> Self { + Self(U8x20::from(value)) + } +} + +impl From for P2WPKHBytes { + fn from(value: U8x20) -> Self { + Self(value) + } +} + +impl fmt::Display for P2WPKHBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} diff --git a/crates/brk_structs/src/p2wshbytes.rs b/crates/brk_structs/src/p2wshbytes.rs new file mode 100644 index 000000000..9ee83a406 --- /dev/null +++ b/crates/brk_structs/src/p2wshbytes.rs @@ -0,0 +1,30 @@ +use std::fmt; + +use derive_deref::Deref; +use serde::Serialize; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +use crate::U8x32; + +#[derive( + Debug, Clone, Deref, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes, Serialize, +)] +pub struct P2WSHBytes(U8x32); + +impl From<&[u8]> for P2WSHBytes { + fn from(value: &[u8]) -> Self { + Self(U8x32::from(value)) + } +} + +impl From for P2WSHBytes { + fn from(value: U8x32) -> Self { + Self(value) + } +} + +impl fmt::Display for P2WSHBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self.0) + } +} diff --git a/crates/brk_structs/src/rawlocktime.rs b/crates/brk_structs/src/rawlocktime.rs index 081d15720..f4cb8e6f4 100644 --- a/crates/brk_structs/src/rawlocktime.rs +++ b/crates/brk_structs/src/rawlocktime.rs @@ -1,10 +1,21 @@ use bitcoin::{absolute::LockTime, locktime::absolute::LOCK_TIME_THRESHOLD}; +use schemars::JsonSchema; use serde::Serialize; use vecdb::StoredCompressed; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; +/// Transaction locktime #[derive( - Debug, Immutable, Clone, Copy, IntoBytes, KnownLayout, FromBytes, Serialize, StoredCompressed, + Debug, + Immutable, + Clone, + Copy, + IntoBytes, + KnownLayout, + FromBytes, + Serialize, + StoredCompressed, + JsonSchema, )] pub struct RawLockTime(u32); diff --git a/crates/brk_structs/src/sats.rs b/crates/brk_structs/src/sats.rs index 063177323..1ced689c0 100644 --- a/crates/brk_structs/src/sats.rs +++ b/crates/brk_structs/src/sats.rs @@ -15,6 +15,7 @@ use crate::StoredF64; use super::{Bitcoin, Cents, Dollars, Height}; +/// Satoshis #[derive( Debug, PartialEq, diff --git a/crates/brk_structs/src/timestamp.rs b/crates/brk_structs/src/timestamp.rs index 384802339..9575b38d3 100644 --- a/crates/brk_structs/src/timestamp.rs +++ b/crates/brk_structs/src/timestamp.rs @@ -3,12 +3,14 @@ use std::ops::{Add, AddAssign, Div}; use allocative::Allocative; use derive_deref::Deref; use jiff::{civil::date, tz::TimeZone}; +use schemars::JsonSchema; use serde::Serialize; use vecdb::{CheckedSub, StoredCompressed}; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; use super::Date; +/// Timestamp #[derive( Debug, Deref, @@ -25,6 +27,7 @@ use super::Date; Serialize, StoredCompressed, Allocative, + JsonSchema, )] pub struct Timestamp(u32); diff --git a/crates/brk_structs/src/tx.rs b/crates/brk_structs/src/tx.rs new file mode 100644 index 000000000..9e5ba78c6 --- /dev/null +++ b/crates/brk_structs/src/tx.rs @@ -0,0 +1,43 @@ +use crate::{RawLockTime, Sats, TxIndex, TxInput, TxOutput, TxStatus, TxVersion, Txid}; +use schemars::JsonSchema; +use serde::Serialize; + +#[derive(Debug, Serialize, JsonSchema)] +/// Transaction information compatible with mempool.space API format +pub struct Tx { + #[schemars(example = "9a0b3b8305bb30cacf9e8443a90d53a76379fb3305047fdeaa4e4b0934a2a1ba")] + pub txid: Txid, + + #[schemars(example = TxIndex::new(0))] + pub index: TxIndex, + + #[schemars(example = 2)] + pub version: TxVersion, + + #[schemars(example = 0)] + pub locktime: RawLockTime, + + /// Transaction size in bytes + #[schemars(example = 222)] + pub size: u32, + + /// Transaction weight (for SegWit transactions) + #[schemars(example = 558)] + pub weight: u32, + + /// Number of signature operations + #[schemars(example = 1)] + pub sigops: u32, + + /// Transaction fee in satoshis + #[schemars(example = Sats::new(31))] + pub fee: Sats, + + /// Transaction inputs + pub vin: Vec, + + /// Transaction outputs + pub vout: Vec, + + pub status: TxStatus, +} diff --git a/crates/brk_structs/src/txid.rs b/crates/brk_structs/src/txid.rs index dff75e10b..f161e387e 100644 --- a/crates/brk_structs/src/txid.rs +++ b/crates/brk_structs/src/txid.rs @@ -6,9 +6,15 @@ use schemars::JsonSchema; use serde::{Serialize, Serializer}; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; +/// Transaction ID (hash) #[derive( Debug, Deref, Clone, PartialEq, Eq, Immutable, IntoBytes, KnownLayout, FromBytes, JsonSchema, )] +#[schemars( + example = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b", + example = "2bb85f4b004be6da54f766c17c1e855187327112c231ef2ff35ebad0ea67c69e", + example = "9a0b3b8305bb30cacf9e8443a90d53a76379fb3305047fdeaa4e4b0934a2a1ba" +)] pub struct Txid([u8; 32]); impl From for Txid { diff --git a/crates/brk_structs/src/txinfo.rs b/crates/brk_structs/src/txinfo.rs deleted file mode 100644 index 4006ee693..000000000 --- a/crates/brk_structs/src/txinfo.rs +++ /dev/null @@ -1,19 +0,0 @@ -use schemars::JsonSchema; -use serde::Serialize; - -use crate::{TxIndex, Txid}; - -#[derive(Serialize, JsonSchema)] -/// Transaction Information -pub struct TransactionInfo { - #[schemars( - with = "String", - example = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b" - )] - pub txid: Txid, - #[schemars(example = TxIndex::new(0))] - pub index: TxIndex, - // #[serde(flatten)] - // #[schemars(with = "serde_json::Value")] - // pub tx: Transaction, -} diff --git a/crates/brk_structs/src/txinput.rs b/crates/brk_structs/src/txinput.rs new file mode 100644 index 000000000..fd481319d --- /dev/null +++ b/crates/brk_structs/src/txinput.rs @@ -0,0 +1,40 @@ +use crate::{TxPrevout, Txid, Vout}; +use schemars::JsonSchema; +use serde::Serialize; + +#[derive(Debug, Serialize, JsonSchema)] +/// Transaction input +pub struct TxInput { + /// Transaction ID of the output being spent + pub txid: Txid, + + #[schemars(example = 0)] + pub vout: Vout, + + /// Information about the previous output being spent + pub prevout: Option, + + /// Signature script (for non-SegWit inputs) + #[schemars(example = "")] + pub scriptsig: String, + + /// Signature script in assembly format + #[schemars(example = "")] + pub scriptsig_asm: String, + + /// Witness data (for SegWit inputs) + #[schemars(example = vec!["3045022100d0c9936990bf00bdba15f425f0f360a223d5cbf81f4bf8477fe6c6d838fb5fae02207e42a8325a4dd41702bf065aa6e0a1b7b0b8ee92a5e6c182da018b0afc82c40601".to_string()])] + pub witness: Vec, + + /// Whether this input is a coinbase (block reward) input + #[schemars(example = false)] + pub is_coinbase: bool, + + /// Input sequence number + #[schemars(example = 429496729)] + pub sequence: u32, + + /// Inner redeemscript in assembly format (for P2SH-wrapped SegWit) + #[schemars(example = Some("OP_0 OP_PUSHBYTES_20 992a1f7420fc5285070d19c71ff2efb1e356ad2f".to_string()))] + pub inner_redeemscript_asm: Option, +} diff --git a/crates/brk_structs/src/txoutput.rs b/crates/brk_structs/src/txoutput.rs new file mode 100644 index 000000000..41c5bd145 --- /dev/null +++ b/crates/brk_structs/src/txoutput.rs @@ -0,0 +1,27 @@ +use crate::Sats; +use schemars::JsonSchema; +use serde::Serialize; + +#[derive(Debug, Serialize, JsonSchema)] +/// Transaction output +pub struct TxOutput { + /// Script pubkey (locking script) + #[schemars(example = "00143b064c595a95f977f00352d6e917501267cacdc6")] + pub scriptpubkey: String, + + /// Script pubkey in assembly format + #[schemars(example = "OP_0 OP_PUSHBYTES_20 3b064c595a95f977f00352d6e917501267cacdc6")] + pub scriptpubkey_asm: String, + + /// Script type (p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr, op_return, etc.) + #[schemars(example = &"v0_p2wpkh")] + pub scriptpubkey_type: String, + + /// Bitcoin address (if applicable, None for OP_RETURN) + #[schemars(example = Some("bc1q8vryck26jhuh0uqr2ttwj96szfnu4nwxfmu39y".to_string()))] + pub scriptpubkey_address: Option, + + /// Value of the output in satoshis + #[schemars(example = Sats::new(7782))] + pub value: Sats, +} diff --git a/crates/brk_structs/src/txprevout.rs b/crates/brk_structs/src/txprevout.rs new file mode 100644 index 000000000..56f7c9fad --- /dev/null +++ b/crates/brk_structs/src/txprevout.rs @@ -0,0 +1,27 @@ +use crate::Sats; +use schemars::JsonSchema; +use serde::Serialize; + +#[derive(Debug, Serialize, JsonSchema)] +/// Information about a previous transaction output being spent +pub struct TxPrevout { + /// Script pubkey (locking script) + #[schemars(example = "00143b064c595a95f977f00352d6e917501267cacdc6")] + pub scriptpubkey: String, + + /// Script pubkey in assembly format + #[schemars(example = "OP_0 OP_PUSHBYTES_20 3b064c595a95f977f00352d6e917501267cacdc6")] + pub scriptpubkey_asm: String, + + /// Script type (p2pk, p2pkh, p2sh, p2wpkh, p2wsh, p2tr, etc.) + #[schemars(example = &"v0_p2wpkh")] + pub scriptpubkey_type: String, + + /// Bitcoin address (if applicable) + #[schemars(example = Some("bc1q8vryck26jhuh0uqr2ttwj96szfnu4nwxfmu39y".to_string()))] + pub scriptpubkey_address: Option, + + /// Value of the output in satoshis + #[schemars(example = Sats::new(7813))] + pub value: Sats, +} diff --git a/crates/brk_structs/src/txstatus.rs b/crates/brk_structs/src/txstatus.rs new file mode 100644 index 000000000..cd1fe634e --- /dev/null +++ b/crates/brk_structs/src/txstatus.rs @@ -0,0 +1,24 @@ +use schemars::JsonSchema; +use serde::Serialize; + +use crate::{BlockHash, Height, Timestamp}; + +#[derive(Debug, Serialize, JsonSchema)] +/// Transaction confirmation status +pub struct TxStatus { + /// Whether the transaction is confirmed + #[schemars(example = true)] + pub confirmed: bool, + + /// Block height (only present if confirmed) + #[schemars(example = Some(916656))] + pub block_height: Option, + + /// Block hash (only present if confirmed) + #[schemars(example = Some("000000000000000000012711f7e0d13e586752a42c66e25faf75f159b3d04911".to_string()))] + pub block_hash: Option, + + /// Block timestamp (only present if confirmed) + #[schemars(example = Some(1759000868))] + pub block_time: Option, +} diff --git a/crates/brk_structs/src/txversion.rs b/crates/brk_structs/src/txversion.rs index 6c49257f6..4098c323c 100644 --- a/crates/brk_structs/src/txversion.rs +++ b/crates/brk_structs/src/txversion.rs @@ -1,10 +1,12 @@ use derive_deref::Deref; +use schemars::JsonSchema; use serde::Serialize; use vecdb::StoredCompressed; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; use super::StoredU16; +/// Transaction version number #[derive( Debug, Deref, @@ -20,6 +22,7 @@ use super::StoredU16; FromBytes, Serialize, StoredCompressed, + JsonSchema, )] pub struct TxVersion(u16); diff --git a/crates/brk_structs/src/vout.rs b/crates/brk_structs/src/vout.rs index 4cc8ef793..01b9dd770 100644 --- a/crates/brk_structs/src/vout.rs +++ b/crates/brk_structs/src/vout.rs @@ -1,6 +1,9 @@ use derive_deref::Deref; +use schemars::JsonSchema; +use serde::Serialize; -#[derive(Debug, Deref, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +/// Index of the output being spent in the previous transaction +#[derive(Debug, Deref, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, JsonSchema)] pub struct Vout(u32); impl Vout {