global: snapshot

This commit is contained in:
nym21
2025-10-13 13:52:33 +02:00
parent 7bfca87caf
commit db0298ac1b
58 changed files with 1094 additions and 653 deletions

13
Cargo.lock generated
View File

@@ -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]]

View File

@@ -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" }

View File

@@ -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);

View File

@@ -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);

View File

@@ -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<bitcoin::consensus::encode::Error> for Error {
}
}
impl From<bitcoin::address::FromScriptError> for Error {
fn from(value: bitcoin::address::FromScriptError) -> Self {
Self::BitcoinFromScriptError(value)
}
}
impl From<time::SystemTimeError> 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),

View File

@@ -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<AddressInfo> {
pub fn get_address(Address { address }: Address, interface: &Interface) -> Result<AddressStats> {
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()),
// })
}

View File

@@ -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<TransactionInfo> {
pub fn get_transaction_info(TxidPath { txid }: TxidPath, interface: &Interface) -> Result<Tx> {
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
// })
}

View File

@@ -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<AddressInfo> {
get_address_info(address, self)
pub fn get_address(&self, address: Address) -> Result<AddressStats> {
get_address(address, self)
}
pub fn get_transaction_info(&self, txid: TxidPath) -> Result<TransactionInfo> {
pub fn get_transaction_info(&self, txid: TxidPath) -> Result<Tx> {
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(

View File

@@ -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,
}

View File

@@ -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<'_> {

View File

@@ -1 +0,0 @@
# brk_mempool

View File

@@ -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<FxHashMap<String, Transaction>>,
}
impl Mempool {
pub fn new(rpc: &'static Client) -> Self {
Self {
rpc,
txs: RwLock::new(FxHashMap::default()),
}
}
pub fn get_txs(&self) -> RwLockReadGuard<'_, FxHashMap<String, Transaction>> {
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<dyn std::error::Error>> {
let current_txids = self.rpc.get_raw_mempool()?;
let current_set: std::collections::HashSet<String> =
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(())
}
}

View File

@@ -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 }

View File

@@ -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.

View File

@@ -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<FxHashMap<Txid, Transaction>>,
}
impl Mempool {
pub fn new(rpc: &'static Client) -> Self {
Self {
rpc,
txs: RwLock::new(FxHashMap::default()),
}
}
pub fn get_txs(&self) -> RwLockReadGuard<'_, FxHashMap<Txid, Transaction>> {
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<dyn std::error::Error>> {
let txids = self
.rpc
.get_raw_mempool()?
.into_iter()
.collect::<FxHashSet<_>>();
let missing_txids = {
let txs = self.txs.read();
txids
.iter()
.filter(|txid| !txs.contains_key(*txid))
.take(MAX_FETCHES_PER_CYCLE)
.collect::<Vec<_>>()
};
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::<FxHashMap<_, _>>();
let mut txs = self.txs.write();
txs.retain(|txid, _| txids.contains(txid));
txs.extend(new_txs);
Ok(())
}
}

View File

@@ -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

View File

@@ -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<AppState> {
"/api/address/{address}",
get_with(async |
headers: HeaderMap,
Path(address): Path<AddressPath>,
Path(address): Path<Address>,
State(state): State<AppState>
| {
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<AppState> {
.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::<AddressInfo>()
.ok_response::<AddressStats>()
.not_modified()
.bad_request()
.not_found()

View File

@@ -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<AppState> {
),
)
.api_route(
"/api/metrics/search",
"/api/metrics/search/{metric}",
get_with(
async |
headers: HeaderMap,
State(state): State<AppState>,
Query(query): Query<MetricSearchQuery>
Path(metric): Path<Metric>,
Query(limit): Query<Limit>
| {
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<AppState> {
async |
headers: HeaderMap,
State(state): State<AppState>,
Path(MetricPath { metric }): Path<MetricPath>
Path(metric): Path<Metric>
| {
let etag = VERSION;
if headers.has_etag(etag) {
@@ -150,10 +151,7 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
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<AppState> {
async |uri: Uri,
headers: HeaderMap,
state: State<AppState>,
Path((metric, index)): Path<(MetricPath, Index)>,
Path((metric, index)): Path<(Metric, Index)>,
Query(params_opt): Query<ParamsOpt>|
-> Response {
todo!();
@@ -232,7 +230,8 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
};
let params = Params::from((
(index, split.collect::<Vec<_>>().join(separator)),
index,
Metrics::from(split.collect::<Vec<_>>().join(separator)),
params_opt,
));
data::handler(uri, headers, Query(params), state).await

View File

@@ -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<AppState> {
.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::<TransactionInfo>()
.ok_response::<Tx>()
.not_modified()
.bad_request()
.not_found()

View File

@@ -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 }

View File

@@ -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<String> for Address {
fn from(address: String) -> Self {
Self { address }
}
}
impl TryFrom<ScriptBuf> for Address {
type Error = Error;
fn try_from(script: ScriptBuf) -> Result<Self, Self::Error> {
Ok(Self::from(bitcoin::Address::from_script(
&script,
Network::Bitcoin,
)?))
}
}
impl From<bitcoin::Address> 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, Self::Error> {
Self::try_from(AddressBytes::try_from(tuple)?)
}
}
impl TryFrom<AddressBytes> for Address {
type Error = Error;
fn try_from(bytes: AddressBytes) -> Result<Self, Self::Error> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&self.address)
}
}

View File

@@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&self.to_string())
}
}
impl From<P2PK65Bytes> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&self.to_string())
}
}
impl From<P2PK33Bytes> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&self.to_string())
}
}
impl From<P2PKHBytes> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&self.to_string())
}
}
impl From<P2SHBytes> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&self.to_string())
}
}
impl From<P2WPKHBytes> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&self.to_string())
}
}
impl From<P2WSHBytes> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&self.to_string())
}
}
impl From<P2TRBytes> 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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&self.to_string())
}
}
impl From<P2ABytes> 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)
}
}

View File

@@ -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<u64>,
/// Index of this address within its type on the blockchain
#[schemars(example = TypeIndex::new(0))]
pub type_index: TypeIndex,
}

View File

@@ -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<Dollars>,
/// 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<Dollars>,
/// 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<Dollars>,
//
// 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
}

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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,
}

View File

@@ -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<bitcoin::BlockHash> for BlockHash {

View File

@@ -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)
}
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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::*;

View File

@@ -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 {

View File

@@ -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<String> 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)
}
}

View File

@@ -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,
}

View File

@@ -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<String>);
pub struct Metrics {
/// A list of metrics
metrics: Vec<Metric>,
}
const MAX_VECS: usize = 32;
const MAX_STRING_SIZE: usize = 64 * MAX_VECS;
impl From<String> for MaybeMetrics {
impl From<Metric> for Metrics {
fn from(metric: Metric) -> Self {
Self {
metrics: vec![metric],
}
}
}
impl From<String> for Metrics {
fn from(value: String) -> Self {
Self(vec![value.replace("-", "_").to_lowercase()])
Self::from(Metric::from(value.replace("-", "_").to_lowercase()))
}
}
impl<'a> From<Vec<&'a str>> for MaybeMetrics {
impl<'a> From<Vec<&'a str>> 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::<Vec<_>>(),
)
}
}
}
impl<'de> Deserialize<'de> for MaybeMetrics {
impl<'de> Deserialize<'de> for Metrics {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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::<Vec<_>>()
.join(",");
write!(f, "{s}")
}
}
fn sanitize_metrics(raw_ids: impl Iterator<Item = String>) -> Vec<String> {
let mut results = Vec::new();
raw_ids.for_each(|s| {
fn sanitize(dirty: impl Iterator<Item = String>) -> Vec<String> {
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<Item = String>) -> Vec<String> {
}
}
if !current.is_empty() {
results.push(current);
clean.push(current);
}
});
results
clean
}

View File

@@ -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<U8x2> 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)
}
}

View File

@@ -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<U8x33> 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)
}
}

View File

@@ -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<U8x65> 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)
}
}

View File

@@ -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<U8x20> 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)
}
}

View File

@@ -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<U8x20> 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)
}
}

View File

@@ -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<U8x32> 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)
}
}

View File

@@ -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<U8x20> 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)
}
}

View File

@@ -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<U8x32> 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)
}
}

View File

@@ -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);

View File

@@ -15,6 +15,7 @@ use crate::StoredF64;
use super::{Bitcoin, Cents, Dollars, Height};
/// Satoshis
#[derive(
Debug,
PartialEq,

View File

@@ -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);

View File

@@ -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<TxInput>,
/// Transaction outputs
pub vout: Vec<TxOutput>,
pub status: TxStatus,
}

View File

@@ -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<bitcoin::Txid> for Txid {

View File

@@ -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,
}

View File

@@ -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<TxPrevout>,
/// 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<String>,
/// 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<String>,
}

View File

@@ -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<String>,
/// Value of the output in satoshis
#[schemars(example = Sats::new(7782))]
pub value: Sats,
}

View File

@@ -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<String>,
/// Value of the output in satoshis
#[schemars(example = Sats::new(7813))]
pub value: Sats,
}

View File

@@ -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<Height>,
/// Block hash (only present if confirmed)
#[schemars(example = Some("000000000000000000012711f7e0d13e586752a42c66e25faf75f159b3d04911".to_string()))]
pub block_hash: Option<BlockHash>,
/// Block timestamp (only present if confirmed)
#[schemars(example = Some(1759000868))]
pub block_time: Option<Timestamp>,
}

View File

@@ -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);

View File

@@ -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 {