mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 14:11:56 -07:00
global: snapshot
This commit is contained in:
@@ -7,7 +7,7 @@ use brk_types::TreeNode;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, GenericSyntax, PatternField, PythonSyntax, build_child_path,
|
||||
generate_leaf_field, prepare_tree_node, to_snake_case,
|
||||
escape_python_keyword, generate_leaf_field, prepare_tree_node, to_snake_case,
|
||||
};
|
||||
|
||||
/// Generate tree classes
|
||||
@@ -70,7 +70,7 @@ fn generate_tree_class(
|
||||
|
||||
let syntax = PythonSyntax;
|
||||
for child in &ctx.children {
|
||||
let field_name_py = to_snake_case(child.name);
|
||||
let field_name_py = escape_python_keyword(&to_snake_case(child.name));
|
||||
|
||||
if child.is_leaf {
|
||||
if let TreeNode::Leaf(leaf) = child.node {
|
||||
|
||||
@@ -306,18 +306,52 @@ fn schema_type_from_schema(schema: &Schema) -> Option<String> {
|
||||
}
|
||||
|
||||
fn schema_to_type_name(schema: &ObjectSchema) -> Option<String> {
|
||||
let schema_type = schema.schema_type.as_ref()?;
|
||||
if let Some(schema_type) = schema.schema_type.as_ref() {
|
||||
return match schema_type {
|
||||
SchemaTypeSet::Single(t) => single_type_to_name(t, schema),
|
||||
SchemaTypeSet::Multiple(types) => {
|
||||
// For nullable types like ["integer", "null"], return the non-null type
|
||||
types
|
||||
.iter()
|
||||
.find(|t| !matches!(t, SchemaType::Null))
|
||||
.and_then(|t| single_type_to_name(t, schema))
|
||||
.or(Some("*".to_string()))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
match schema_type {
|
||||
SchemaTypeSet::Single(t) => single_type_to_name(t, schema),
|
||||
SchemaTypeSet::Multiple(types) => {
|
||||
// For nullable types like ["integer", "null"], return the non-null type
|
||||
types
|
||||
.iter()
|
||||
.find(|t| !matches!(t, SchemaType::Null))
|
||||
.and_then(|t| single_type_to_name(t, schema))
|
||||
.or(Some("*".to_string()))
|
||||
}
|
||||
// Handle anyOf/oneOf unions (e.g., Option<RangeIndex> → anyOf: [$ref, null])
|
||||
let variants = if !schema.any_of.is_empty() {
|
||||
&schema.any_of
|
||||
} else if !schema.one_of.is_empty() {
|
||||
&schema.one_of
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let types: Vec<String> = variants
|
||||
.iter()
|
||||
.filter_map(|v| match v {
|
||||
ObjectOrReference::Ref { ref_path, .. } => {
|
||||
ref_to_type_name(ref_path).map(|s| s.to_string())
|
||||
}
|
||||
ObjectOrReference::Object(obj) => {
|
||||
// Skip null variants
|
||||
if matches!(
|
||||
obj.schema_type.as_ref(),
|
||||
Some(SchemaTypeSet::Single(SchemaType::Null))
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
schema_to_type_name(obj)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
match types.len() {
|
||||
0 => None,
|
||||
1 => Some(types.into_iter().next().unwrap()),
|
||||
_ => Some(types.join(" | ")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Basic example of using the BRK client.
|
||||
|
||||
use brk_client::{BrkClient, BrkClientOptions};
|
||||
use brk_types::{FormatResponse, Index, Metric};
|
||||
use brk_types::{FormatResponse, Index, Metric, RangeIndex};
|
||||
|
||||
fn main() -> brk_client::Result<()> {
|
||||
// Create client with default options
|
||||
@@ -79,7 +79,7 @@ fn main() -> brk_client::Result<()> {
|
||||
let metricdata = client.get_metric(
|
||||
Metric::from("price_close"),
|
||||
Index::Day1,
|
||||
Some(-3),
|
||||
Some(RangeIndex::Int(-3)),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! and fetch data from each endpoint. Run with: cargo run --example tree
|
||||
|
||||
use brk_client::BrkClient;
|
||||
use brk_types::{Index, TreeNode};
|
||||
use brk_types::{Index, RangeIndex, TreeNode};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
/// A collected metric with its path and available indexes.
|
||||
@@ -62,7 +62,7 @@ fn main() -> brk_client::Result<()> {
|
||||
metric.name.as_str().into(),
|
||||
*index,
|
||||
None,
|
||||
Some(0),
|
||||
Some(RangeIndex::Int(0)),
|
||||
None,
|
||||
None,
|
||||
) {
|
||||
|
||||
@@ -7860,17 +7860,16 @@ impl BrkClient {
|
||||
self.base.get_json(&format!("/api/address/{address}"))
|
||||
}
|
||||
|
||||
/// Address transaction IDs
|
||||
/// Address transactions
|
||||
///
|
||||
/// Get transaction IDs for an address, newest first. Use after_txid for pagination.
|
||||
/// Get transaction history for an address, sorted with newest first. Returns up to 50 mempool transactions plus the first 25 confirmed transactions. Use ?after_txid=<txid> for pagination.
|
||||
///
|
||||
/// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*
|
||||
///
|
||||
/// Endpoint: `GET /api/address/{address}/txs`
|
||||
pub fn get_address_txs(&self, address: Address, after_txid: Option<&str>, limit: Option<i64>) -> Result<Vec<Txid>> {
|
||||
pub fn get_address_txs(&self, address: Address, after_txid: Option<Txid>) -> Result<Vec<Transaction>> {
|
||||
let mut query = Vec::new();
|
||||
if let Some(v) = after_txid { query.push(format!("after_txid={}", v)); }
|
||||
if let Some(v) = limit { query.push(format!("limit={}", v)); }
|
||||
let query_str = if query.is_empty() { String::new() } else { format!("?{}", query.join("&")) };
|
||||
let path = format!("/api/address/{address}/txs{}", query_str);
|
||||
self.base.get_json(&path)
|
||||
@@ -7878,15 +7877,14 @@ impl BrkClient {
|
||||
|
||||
/// Address confirmed transactions
|
||||
///
|
||||
/// Get confirmed transaction IDs for an address, 25 per page. Use ?after_txid=<txid> for pagination.
|
||||
/// Get confirmed transactions for an address, 25 per page. Use ?after_txid=<txid> for pagination.
|
||||
///
|
||||
/// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)*
|
||||
///
|
||||
/// Endpoint: `GET /api/address/{address}/txs/chain`
|
||||
pub fn get_address_confirmed_txs(&self, address: Address, after_txid: Option<&str>, limit: Option<i64>) -> Result<Vec<Txid>> {
|
||||
pub fn get_address_confirmed_txs(&self, address: Address, after_txid: Option<Txid>) -> Result<Vec<Transaction>> {
|
||||
let mut query = Vec::new();
|
||||
if let Some(v) = after_txid { query.push(format!("after_txid={}", v)); }
|
||||
if let Some(v) = limit { query.push(format!("limit={}", v)); }
|
||||
let query_str = if query.is_empty() { String::new() } else { format!("?{}", query.join("&")) };
|
||||
let path = format!("/api/address/{address}/txs/chain{}", query_str);
|
||||
self.base.get_json(&path)
|
||||
@@ -8058,7 +8056,7 @@ impl BrkClient {
|
||||
/// Fetch data for a specific metric at the given index. Use query parameters to filter by date range and format (json/csv).
|
||||
///
|
||||
/// Endpoint: `GET /api/metric/{metric}/{index}`
|
||||
pub fn get_metric(&self, metric: Metric, index: Index, start: Option<&str>, end: Option<&str>, limit: Option<&str>, format: Option<Format>) -> Result<FormatResponse<MetricData>> {
|
||||
pub fn get_metric(&self, metric: Metric, index: Index, start: Option<RangeIndex>, end: Option<RangeIndex>, limit: Option<Limit>, format: Option<Format>) -> Result<FormatResponse<MetricData>> {
|
||||
let mut query = Vec::new();
|
||||
if let Some(v) = start { query.push(format!("start={}", v)); }
|
||||
if let Some(v) = end { query.push(format!("end={}", v)); }
|
||||
@@ -8078,7 +8076,7 @@ impl BrkClient {
|
||||
/// Returns just the data array without the MetricData wrapper. Supports the same range and format parameters as the standard endpoint.
|
||||
///
|
||||
/// Endpoint: `GET /api/metric/{metric}/{index}/data`
|
||||
pub fn get_metric_data(&self, metric: Metric, index: Index, start: Option<&str>, end: Option<&str>, limit: Option<&str>, format: Option<Format>) -> Result<FormatResponse<Vec<bool>>> {
|
||||
pub fn get_metric_data(&self, metric: Metric, index: Index, start: Option<RangeIndex>, end: Option<RangeIndex>, limit: Option<Limit>, format: Option<Format>) -> Result<FormatResponse<Vec<bool>>> {
|
||||
let mut query = Vec::new();
|
||||
if let Some(v) = start { query.push(format!("start={}", v)); }
|
||||
if let Some(v) = end { query.push(format!("end={}", v)); }
|
||||
@@ -8134,7 +8132,7 @@ impl BrkClient {
|
||||
/// Fetch multiple metrics in a single request. Supports filtering by index and date range. Returns an array of MetricData objects. For a single metric, use `get_metric` instead.
|
||||
///
|
||||
/// Endpoint: `GET /api/metrics/bulk`
|
||||
pub fn get_metrics(&self, metrics: Metrics, index: Index, start: Option<&str>, end: Option<&str>, limit: Option<&str>, format: Option<Format>) -> Result<FormatResponse<Vec<MetricData>>> {
|
||||
pub fn get_metrics(&self, metrics: Metrics, index: Index, start: Option<RangeIndex>, end: Option<RangeIndex>, limit: Option<Limit>, format: Option<Format>) -> Result<FormatResponse<Vec<MetricData>>> {
|
||||
let mut query = Vec::new();
|
||||
query.push(format!("metrics={}", metrics));
|
||||
query.push(format!("index={}", index));
|
||||
@@ -8210,9 +8208,10 @@ impl BrkClient {
|
||||
/// Paginated flat list of all available metric names. Use `page` query param for pagination.
|
||||
///
|
||||
/// Endpoint: `GET /api/metrics/list`
|
||||
pub fn list_metrics(&self, page: Option<i64>) -> Result<PaginatedMetrics> {
|
||||
pub fn list_metrics(&self, page: Option<i64>, per_page: Option<i64>) -> Result<PaginatedMetrics> {
|
||||
let mut query = Vec::new();
|
||||
if let Some(v) = page { query.push(format!("page={}", v)); }
|
||||
if let Some(v) = per_page { query.push(format!("per_page={}", v)); }
|
||||
let query_str = if query.is_empty() { String::new() } else { format!("?{}", query.join("&")) };
|
||||
let path = format!("/api/metrics/list{}", query_str);
|
||||
self.base.get_json(&path)
|
||||
|
||||
@@ -96,9 +96,7 @@ impl ActivityCore {
|
||||
|
||||
sum_others!(self, starting_indexes, others, exit; coindays_destroyed.raw.height);
|
||||
sum_others!(self, starting_indexes, others, exit; sent_in_profit.raw.sats.height);
|
||||
sum_others!(self, starting_indexes, others, exit; sent_in_profit.raw.cents.height);
|
||||
sum_others!(self, starting_indexes, others, exit; sent_in_loss.raw.sats.height);
|
||||
sum_others!(self, starting_indexes, others, exit; sent_in_loss.raw.cents.height);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -235,9 +235,13 @@ pub struct MetricNotFound {
|
||||
}
|
||||
|
||||
impl MetricNotFound {
|
||||
pub fn new(metric: String, all_matches: Vec<String>) -> Self {
|
||||
pub fn new(mut metric: String, all_matches: Vec<String>) -> Self {
|
||||
let total_matches = all_matches.len();
|
||||
let suggestions = all_matches.into_iter().take(3).collect();
|
||||
if metric.len() > 100 {
|
||||
metric.truncate(100);
|
||||
metric.push_str("...");
|
||||
}
|
||||
Self {
|
||||
metric,
|
||||
suggestions,
|
||||
@@ -261,7 +265,7 @@ impl fmt::Display for MetricNotFound {
|
||||
if remaining > 0 {
|
||||
write!(
|
||||
f,
|
||||
" ({remaining} more — /api/metrics/search/{} for all)",
|
||||
" ({remaining} more — /api/metrics/search?q={} for all)",
|
||||
self.metric
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use brk_types::{
|
||||
use rayon::prelude::*;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
use bitcoin::ScriptBuf;
|
||||
use vecdb::{
|
||||
AnyStoredVec, BytesVec, BytesVecValue, Database, Formattable, ImportableVec, PcoVec,
|
||||
PcoVecValue, ReadableVec, Rw, Stamp, StorageMode, VecIndex, WritableVec,
|
||||
@@ -257,3 +258,20 @@ impl AddressesVecs {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: StorageMode> AddressesVecs<M> {
|
||||
pub fn script_pubkey(&self, outputtype: OutputType, typeindex: TypeIndex) -> ScriptBuf {
|
||||
let bytes: Option<AddressBytes> = match outputtype {
|
||||
OutputType::P2PK65 => self.p2pk65.bytes.collect_one(typeindex.into()).map(Into::into),
|
||||
OutputType::P2PK33 => self.p2pk33.bytes.collect_one(typeindex.into()).map(Into::into),
|
||||
OutputType::P2PKH => self.p2pkh.bytes.collect_one(typeindex.into()).map(Into::into),
|
||||
OutputType::P2SH => self.p2sh.bytes.collect_one(typeindex.into()).map(Into::into),
|
||||
OutputType::P2WPKH => self.p2wpkh.bytes.collect_one(typeindex.into()).map(Into::into),
|
||||
OutputType::P2WSH => self.p2wsh.bytes.collect_one(typeindex.into()).map(Into::into),
|
||||
OutputType::P2TR => self.p2tr.bytes.collect_one(typeindex.into()).map(Into::into),
|
||||
OutputType::P2A => self.p2a.bytes.collect_one(typeindex.into()).map(Into::into),
|
||||
_ => None,
|
||||
};
|
||||
bytes.map(|b| b.to_script_pubkey()).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ use bitcoin::{Network, PublicKey, ScriptBuf};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::{
|
||||
Address, AddressBytes, AddressChainStats, AddressHash, AddressIndexOutPoint,
|
||||
AddressIndexTxIndex, AddressStats, AnyAddressDataIndexEnum, OutputType, Sats, TxIndex,
|
||||
TxStatus, Txid, TypeIndex, Unit, Utxo, Vout,
|
||||
AddressIndexTxIndex, AddressStats, AnyAddressDataIndexEnum, OutputType, Sats, Transaction,
|
||||
TxIndex, TxStatus, Txid, TypeIndex, Unit, Utxo, Vout,
|
||||
};
|
||||
use vecdb::{ReadableVec, VecIndex};
|
||||
|
||||
@@ -32,16 +32,12 @@ impl Query {
|
||||
return Err(Error::InvalidAddress);
|
||||
};
|
||||
|
||||
dbg!(&script);
|
||||
|
||||
let outputtype = OutputType::from(&script);
|
||||
dbg!(outputtype);
|
||||
let Ok(bytes) = AddressBytes::try_from((&script, outputtype)) else {
|
||||
return Err(Error::InvalidAddress);
|
||||
};
|
||||
let addresstype = outputtype;
|
||||
let hash = AddressHash::from(&bytes);
|
||||
dbg!(hash);
|
||||
|
||||
let Ok(Some(type_index)) = stores
|
||||
.addresstype_to_addresshash_to_addressindex
|
||||
@@ -94,16 +90,44 @@ impl Query {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn address_txs(
|
||||
&self,
|
||||
address: Address,
|
||||
after_txid: Option<Txid>,
|
||||
limit: usize,
|
||||
) -> Result<Vec<Transaction>> {
|
||||
let txindices = self.address_txindices(&address, after_txid, limit)?;
|
||||
txindices
|
||||
.into_iter()
|
||||
.map(|txindex| self.transaction_by_index(txindex))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn address_txids(
|
||||
&self,
|
||||
address: Address,
|
||||
after_txid: Option<Txid>,
|
||||
limit: usize,
|
||||
) -> Result<Vec<Txid>> {
|
||||
let txindices = self.address_txindices(&address, after_txid, limit)?;
|
||||
let txid_reader = self.indexer().vecs.transactions.txid.reader();
|
||||
let txids = txindices
|
||||
.into_iter()
|
||||
.map(|txindex| txid_reader.get(txindex.to_usize()))
|
||||
.collect();
|
||||
Ok(txids)
|
||||
}
|
||||
|
||||
fn address_txindices(
|
||||
&self,
|
||||
address: &Address,
|
||||
after_txid: Option<Txid>,
|
||||
limit: usize,
|
||||
) -> Result<Vec<TxIndex>> {
|
||||
let indexer = self.indexer();
|
||||
let stores = &indexer.stores;
|
||||
|
||||
let (outputtype, type_index) = self.resolve_address(&address)?;
|
||||
let (outputtype, type_index) = self.resolve_address(address)?;
|
||||
|
||||
let store = stores
|
||||
.addresstype_to_addressindex_and_txindex
|
||||
@@ -124,7 +148,7 @@ impl Query {
|
||||
None
|
||||
};
|
||||
|
||||
let txindices: Vec<TxIndex> = store
|
||||
Ok(store
|
||||
.prefix(prefix)
|
||||
.rev()
|
||||
.filter(|(key, _): &(AddressIndexTxIndex, Unit)| {
|
||||
@@ -136,15 +160,7 @@ impl Query {
|
||||
})
|
||||
.take(limit)
|
||||
.map(|(key, _)| key.txindex())
|
||||
.collect();
|
||||
|
||||
let txid_reader = indexer.vecs.transactions.txid.reader();
|
||||
let txids: Vec<Txid> = txindices
|
||||
.into_iter()
|
||||
.map(|txindex| txid_reader.get(txindex.to_usize()))
|
||||
.collect();
|
||||
|
||||
Ok(txids)
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn address_utxos(&self, address: Address) -> Result<Vec<Utxo>> {
|
||||
|
||||
@@ -35,7 +35,7 @@ impl Query {
|
||||
if let Some(indexes) = self.vecs().metric_to_indexes(metric.clone()) {
|
||||
let supported = indexes
|
||||
.iter()
|
||||
.map(|i| format!("/api/metric/{metric}/{i}"))
|
||||
.map(|i| format!("/api/metric/{metric}/{}", i.name()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
return Error::MetricUnsupportedIndex {
|
||||
@@ -253,7 +253,12 @@ impl Query {
|
||||
}
|
||||
|
||||
/// Format a resolved query as raw data (just the JSON array, no MetricData wrapper).
|
||||
/// CSV output is identical to `format` (no wrapper distinction for CSV).
|
||||
pub fn format_raw(&self, resolved: ResolvedQuery) -> Result<MetricOutput> {
|
||||
if resolved.format() == Format::CSV {
|
||||
return self.format(resolved);
|
||||
}
|
||||
|
||||
let ResolvedQuery {
|
||||
vecs, version, total, start, end, ..
|
||||
} = resolved;
|
||||
|
||||
@@ -3,8 +3,8 @@ use std::io::Cursor;
|
||||
use bitcoin::{consensus::Decodable, hex::DisplayHex};
|
||||
use brk_error::{Error, Result};
|
||||
use brk_types::{
|
||||
Sats, Transaction, TxIn, TxInIndex, TxIndex, TxOut, TxOutspend, TxStatus, Txid, TxidParam,
|
||||
TxidPrefix, Vin, Vout, Weight,
|
||||
OutputType, Sats, Transaction, TxIn, TxInIndex, TxIndex, TxOut, TxOutspend, TxStatus, Txid,
|
||||
TxidParam, TxidPrefix, Vin, Vout, Weight,
|
||||
};
|
||||
use vecdb::{ReadableVec, VecIndex};
|
||||
|
||||
@@ -242,6 +242,8 @@ impl Query {
|
||||
let txid_reader = indexer.vecs.transactions.txid.reader();
|
||||
let first_txoutindex_reader = indexer.vecs.transactions.first_txoutindex.reader();
|
||||
let value_reader = indexer.vecs.outputs.value.reader();
|
||||
let outputtype_reader = indexer.vecs.outputs.outputtype.reader();
|
||||
let typeindex_reader = indexer.vecs.outputs.typeindex.reader();
|
||||
|
||||
// Batch-read outpoints for all inputs (avoids per-input PcoVec page decompression)
|
||||
let outpoints: Vec<_> = indexer.vecs.inputs.outpoint.collect_range_at(
|
||||
@@ -272,13 +274,16 @@ impl Query {
|
||||
first_txoutindex_reader.get(prev_txindex.to_usize());
|
||||
let prev_txoutindex = prev_first_txoutindex + prev_vout;
|
||||
|
||||
// Get the value of the prevout
|
||||
let prev_value = value_reader.get(usize::from(prev_txoutindex));
|
||||
let prev_outputtype: OutputType =
|
||||
outputtype_reader.get(usize::from(prev_txoutindex));
|
||||
let prev_typeindex = typeindex_reader.get(usize::from(prev_txoutindex));
|
||||
let script_pubkey = indexer
|
||||
.vecs
|
||||
.addresses
|
||||
.script_pubkey(prev_outputtype, prev_typeindex);
|
||||
|
||||
let prevout = Some(TxOut::from((
|
||||
bitcoin::ScriptBuf::new(), // Placeholder - would need to reconstruct
|
||||
prev_value,
|
||||
)));
|
||||
let prevout = Some(TxOut::from((script_pubkey, prev_value)));
|
||||
|
||||
(prev_txid, prev_vout, prevout)
|
||||
};
|
||||
|
||||
@@ -147,12 +147,17 @@ impl<'a> Vecs<'a> {
|
||||
|
||||
pub fn metrics(&'static self, pagination: Pagination) -> PaginatedMetrics {
|
||||
let len = self.metrics.len();
|
||||
let per_page = pagination.per_page();
|
||||
let start = pagination.start(len);
|
||||
let end = pagination.end(len);
|
||||
let max_page = len.div_ceil(per_page).saturating_sub(1);
|
||||
|
||||
PaginatedMetrics {
|
||||
current_page: pagination.page(),
|
||||
max_page: len.div_ceil(Pagination::PER_PAGE).saturating_sub(1),
|
||||
max_page,
|
||||
total_count: len,
|
||||
per_page,
|
||||
has_more: pagination.page() < max_page,
|
||||
metrics: self.metrics[start..end]
|
||||
.iter()
|
||||
.map(|&s| Cow::Borrowed(s))
|
||||
@@ -183,6 +188,9 @@ impl<'a> Vecs<'a> {
|
||||
}
|
||||
|
||||
pub fn matches(&self, metric: &Metric, limit: Limit) -> Vec<&'_ str> {
|
||||
if limit.is_zero() {
|
||||
return Vec::new();
|
||||
}
|
||||
self.matcher
|
||||
.as_ref()
|
||||
.expect("matcher not initialized")
|
||||
|
||||
@@ -6,7 +6,7 @@ use axum::{
|
||||
routing::get,
|
||||
};
|
||||
use brk_types::{
|
||||
AddressParam, AddressStats, AddressTxidsParam, AddressValidation, Txid, Utxo,
|
||||
AddressParam, AddressStats, AddressTxidsParam, AddressValidation, Transaction, Txid, Utxo,
|
||||
ValidateAddressParam,
|
||||
};
|
||||
|
||||
@@ -53,13 +53,13 @@ impl AddressRoutes for ApiRouter<AppState> {
|
||||
Query(params): Query<AddressTxidsParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_txids(path.address, params.after_txid, params.limit)).await
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_txs(path.address, params.after_txid, 25)).await
|
||||
}, |op| op
|
||||
.id("get_address_txs")
|
||||
.addresses_tag()
|
||||
.summary("Address transaction IDs")
|
||||
.description("Get transaction IDs for an address, newest first. Use after_txid for pagination.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*")
|
||||
.ok_response::<Vec<Txid>>()
|
||||
.summary("Address transactions")
|
||||
.description("Get transaction history for an address, sorted with newest first. Returns up to 50 mempool transactions plus the first 25 confirmed transactions. Use ?after_txid=<txid> for pagination.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*")
|
||||
.ok_response::<Vec<Transaction>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
@@ -67,20 +67,21 @@ impl AddressRoutes for ApiRouter<AppState> {
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/address/{address}/utxo",
|
||||
"/api/address/{address}/txs/chain",
|
||||
get_with(async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<AddressParam>,
|
||||
Query(params): Query<AddressTxidsParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_utxos(path.address)).await
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_txs(path.address, params.after_txid, 25)).await
|
||||
}, |op| op
|
||||
.id("get_address_utxos")
|
||||
.id("get_address_confirmed_txs")
|
||||
.addresses_tag()
|
||||
.summary("Address UTXOs")
|
||||
.description("Get unspent transaction outputs (UTXOs) for an address. Returns txid, vout, value, and confirmation status for each UTXO.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-utxo)*")
|
||||
.ok_response::<Vec<Utxo>>()
|
||||
.summary("Address confirmed transactions")
|
||||
.description("Get confirmed transactions for an address, 25 per page. Use ?after_txid=<txid> for pagination.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)*")
|
||||
.ok_response::<Vec<Transaction>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
@@ -109,21 +110,20 @@ impl AddressRoutes for ApiRouter<AppState> {
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/address/{address}/txs/chain",
|
||||
"/api/address/{address}/utxo",
|
||||
get_with(async |
|
||||
uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<AddressParam>,
|
||||
Query(params): Query<AddressTxidsParam>,
|
||||
State(state): State<AppState>
|
||||
| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_txids(path.address, params.after_txid, 25)).await
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.address_utxos(path.address)).await
|
||||
}, |op| op
|
||||
.id("get_address_confirmed_txs")
|
||||
.id("get_address_utxos")
|
||||
.addresses_tag()
|
||||
.summary("Address confirmed transactions")
|
||||
.description("Get confirmed transaction IDs for an address, 25 per page. Use ?after_txid=<txid> for pagination.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)*")
|
||||
.ok_response::<Vec<Txid>>()
|
||||
.summary("Address UTXOs")
|
||||
.description("Get unspent transaction outputs (UTXOs) for an address. Returns txid, vout, value, and confirmation status for each UTXO.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-utxo)*")
|
||||
.ok_response::<Vec<Utxo>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
|
||||
@@ -38,6 +38,53 @@ impl BlockRoutes for ApiRouter<AppState> {
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/blocks/{height}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<HeightParam>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks(Some(path.height))).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_blocks_from_height")
|
||||
.blocks_tag()
|
||||
.summary("Blocks from height")
|
||||
.description(
|
||||
"Retrieve up to 10 blocks going backwards from the given height. For example, height=100 returns blocks 100, 99, 98, ..., 91. Height=0 returns only block 0.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-blocks)*",
|
||||
)
|
||||
.ok_response::<Vec<BlockInfo>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/block-height/{height}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<HeightParam>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_by_height(path.height)).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_block_by_height")
|
||||
.blocks_tag()
|
||||
.summary("Block by height")
|
||||
.description(
|
||||
"Retrieve block information by block height. Returns block metadata including hash, timestamp, difficulty, size, weight, and transaction count.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-height)*",
|
||||
)
|
||||
.ok_response::<BlockInfo>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/block/{hash}",
|
||||
get_with(
|
||||
@@ -86,53 +133,6 @@ impl BlockRoutes for ApiRouter<AppState> {
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/block-height/{height}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<HeightParam>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_by_height(path.height)).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_block_by_height")
|
||||
.blocks_tag()
|
||||
.summary("Block by height")
|
||||
.description(
|
||||
"Retrieve block information by block height. Returns block metadata including hash, timestamp, difficulty, size, weight, and transaction count.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-height)*",
|
||||
)
|
||||
.ok_response::<BlockInfo>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/blocks/{height}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<HeightParam>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks(Some(path.height))).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_blocks_from_height")
|
||||
.blocks_tag()
|
||||
.summary("Blocks from height")
|
||||
.description(
|
||||
"Retrieve up to 10 blocks going backwards from the given height. For example, height=100 returns blocks 100, 99, 98, ..., 91. Height=0 returns only block 0.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-blocks)*",
|
||||
)
|
||||
.ok_response::<Vec<BlockInfo>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/block/{hash}/txids",
|
||||
get_with(
|
||||
@@ -206,28 +206,6 @@ impl BlockRoutes for ApiRouter<AppState> {
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/v1/mining/blocks/timestamp/{timestamp}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<TimestampParam>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_by_timestamp(path.timestamp)).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_block_by_timestamp")
|
||||
.blocks_tag()
|
||||
.summary("Block by timestamp")
|
||||
.description("Find the block closest to a given UNIX timestamp.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-timestamp)*")
|
||||
.ok_response::<BlockTimestamp>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/block/{hash}/raw",
|
||||
get_with(
|
||||
@@ -252,5 +230,27 @@ impl BlockRoutes for ApiRouter<AppState> {
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/v1/mining/blocks/timestamp/{timestamp}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
Path(path): Path<TimestampParam>,
|
||||
State(state): State<AppState>| {
|
||||
state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_by_timestamp(path.timestamp)).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_block_by_timestamp")
|
||||
.blocks_tag()
|
||||
.summary("Block by timestamp")
|
||||
.description("Find the block closest to a given UNIX timestamp.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-timestamp)*")
|
||||
.ok_response::<BlockTimestamp>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,22 +51,6 @@ impl MempoolRoutes for ApiRouter<AppState> {
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/v1/fees/recommended",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.recommended_fees()).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_recommended_fees")
|
||||
.mempool_tag()
|
||||
.summary("Recommended fees")
|
||||
.description("Get recommended fee rates for different confirmation targets based on current mempool state.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)*")
|
||||
.ok_response::<RecommendedFees>()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/mempool/price",
|
||||
get_with(
|
||||
@@ -87,6 +71,22 @@ impl MempoolRoutes for ApiRouter<AppState> {
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/v1/fees/recommended",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
state.cached_json(&headers, state.mempool_cache(), &uri, |q| q.recommended_fees()).await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_recommended_fees")
|
||||
.mempool_tag()
|
||||
.summary("Recommended fees")
|
||||
.description("Get recommended fee rates for different confirmation targets based on current mempool state.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)*")
|
||||
.ok_response::<RecommendedFees>()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/v1/fees/mempool-blocks",
|
||||
get_with(
|
||||
|
||||
@@ -171,6 +171,74 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
.server_error(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/metric/{metric}/{index}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
addr: Extension<SocketAddr>,
|
||||
state: State<AppState>,
|
||||
Path(path): Path<MetricWithIndex>,
|
||||
Query(range): Query<DataRangeFormat>|
|
||||
-> Response {
|
||||
data::handler(
|
||||
uri,
|
||||
headers,
|
||||
addr,
|
||||
Query(MetricSelection::from((path.index, path.metric, range))),
|
||||
state,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.id("get_metric")
|
||||
.metrics_tag()
|
||||
.summary("Get metric data")
|
||||
.description(
|
||||
"Fetch data for a specific metric at the given index. \
|
||||
Use query parameters to filter by date range and format (json/csv)."
|
||||
)
|
||||
.ok_response::<MetricData>()
|
||||
.csv_response()
|
||||
.not_modified()
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/metric/{metric}/{index}/data",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
addr: Extension<SocketAddr>,
|
||||
state: State<AppState>,
|
||||
Path(path): Path<MetricWithIndex>,
|
||||
Query(range): Query<DataRangeFormat>|
|
||||
-> Response {
|
||||
data::raw_handler(
|
||||
uri,
|
||||
headers,
|
||||
addr,
|
||||
Query(MetricSelection::from((path.index, path.metric, range))),
|
||||
state,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.id("get_metric_data")
|
||||
.metrics_tag()
|
||||
.summary("Get raw metric data")
|
||||
.description(
|
||||
"Returns just the data array without the MetricData wrapper. \
|
||||
Supports the same range and format parameters as the standard endpoint."
|
||||
)
|
||||
.ok_response::<Vec<serde_json::Value>>()
|
||||
.csv_response()
|
||||
.not_modified()
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/metric/{metric}/{index}/latest",
|
||||
get_with(
|
||||
@@ -239,74 +307,6 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/metric/{metric}/{index}/data",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
addr: Extension<SocketAddr>,
|
||||
state: State<AppState>,
|
||||
Path(path): Path<MetricWithIndex>,
|
||||
Query(range): Query<DataRangeFormat>|
|
||||
-> Response {
|
||||
data::raw_handler(
|
||||
uri,
|
||||
headers,
|
||||
addr,
|
||||
Query(MetricSelection::from((path.index, path.metric, range))),
|
||||
state,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.id("get_metric_data")
|
||||
.metrics_tag()
|
||||
.summary("Get raw metric data")
|
||||
.description(
|
||||
"Returns just the data array without the MetricData wrapper. \
|
||||
Supports the same range and format parameters as the standard endpoint."
|
||||
)
|
||||
.ok_response::<Vec<serde_json::Value>>()
|
||||
.csv_response()
|
||||
.not_modified()
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/metric/{metric}/{index}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
addr: Extension<SocketAddr>,
|
||||
state: State<AppState>,
|
||||
Path(path): Path<MetricWithIndex>,
|
||||
Query(range): Query<DataRangeFormat>|
|
||||
-> Response {
|
||||
data::handler(
|
||||
uri,
|
||||
headers,
|
||||
addr,
|
||||
Query(MetricSelection::from((path.index, path.metric, range))),
|
||||
state,
|
||||
)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.id("get_metric")
|
||||
.metrics_tag()
|
||||
.summary("Get metric data")
|
||||
.description(
|
||||
"Fetch data for a specific metric at the given index. \
|
||||
Use query parameters to filter by date range and format (json/csv)."
|
||||
)
|
||||
.ok_response::<MetricData>()
|
||||
.csv_response()
|
||||
.not_modified()
|
||||
.not_found(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/metrics/bulk",
|
||||
get_with(
|
||||
@@ -326,77 +326,6 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/vecs/{variant}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
addr: Extension<SocketAddr>,
|
||||
Path(variant): Path<String>,
|
||||
Query(range): Query<DataRangeFormat>,
|
||||
state: State<AppState>|
|
||||
-> Response {
|
||||
let separator = "_to_";
|
||||
let variant = variant.replace("-", "_");
|
||||
let mut split = variant.split(separator);
|
||||
|
||||
let ser_index = split.next().unwrap();
|
||||
let Ok(index) = Index::try_from(ser_index) else {
|
||||
return Error::not_found(
|
||||
format!("Index '{ser_index}' doesn't exist")
|
||||
).into_response();
|
||||
};
|
||||
|
||||
let params = MetricSelection::from((
|
||||
index,
|
||||
Metrics::from(split.collect::<Vec<_>>().join(separator)),
|
||||
range,
|
||||
));
|
||||
legacy::handler(uri, headers, addr, Query(params), state)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.metrics_tag()
|
||||
.summary("Legacy variant endpoint")
|
||||
.description(
|
||||
"**DEPRECATED** - Use `/api/metric/{metric}/{index}` instead.\n\n\
|
||||
Sunset date: 2027-01-01. May be removed earlier in case of abuse.\n\n\
|
||||
Legacy endpoint for querying metrics by variant path (e.g., `day1_to_price`). \
|
||||
Returns raw data without the MetricData wrapper."
|
||||
)
|
||||
.deprecated()
|
||||
.ok_response::<serde_json::Value>()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/vecs/query",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
addr: Extension<SocketAddr>,
|
||||
Query(params): Query<MetricSelectionLegacy>,
|
||||
state: State<AppState>|
|
||||
-> Response {
|
||||
let params: MetricSelection = params.into();
|
||||
legacy::handler(uri, headers, addr, Query(params), state)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.metrics_tag()
|
||||
.summary("Legacy query endpoint")
|
||||
.description(
|
||||
"**DEPRECATED** - Use `/api/metric/{metric}/{index}` or `/api/metrics/bulk` instead.\n\n\
|
||||
Sunset date: 2027-01-01. May be removed earlier in case of abuse.\n\n\
|
||||
Legacy endpoint for querying metrics. Returns raw data without the MetricData wrapper."
|
||||
)
|
||||
.deprecated()
|
||||
.ok_response::<serde_json::Value>()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
// Cost basis distribution endpoints
|
||||
.api_route(
|
||||
"/api/metrics/cost-basis",
|
||||
@@ -475,5 +404,77 @@ impl ApiMetricsRoutes for ApiRouter<AppState> {
|
||||
},
|
||||
),
|
||||
)
|
||||
// Deprecated endpoints
|
||||
.api_route(
|
||||
"/api/vecs/{variant}",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
addr: Extension<SocketAddr>,
|
||||
Path(variant): Path<String>,
|
||||
Query(range): Query<DataRangeFormat>,
|
||||
state: State<AppState>|
|
||||
-> Response {
|
||||
let separator = "_to_";
|
||||
let variant = variant.replace("-", "_");
|
||||
let mut split = variant.split(separator);
|
||||
|
||||
let ser_index = split.next().unwrap();
|
||||
let Ok(index) = Index::try_from(ser_index) else {
|
||||
return Error::not_found(
|
||||
format!("Index '{ser_index}' doesn't exist")
|
||||
).into_response();
|
||||
};
|
||||
|
||||
let params = MetricSelection::from((
|
||||
index,
|
||||
Metrics::from(split.collect::<Vec<_>>().join(separator)),
|
||||
range,
|
||||
));
|
||||
legacy::handler(uri, headers, addr, Query(params), state)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.metrics_tag()
|
||||
.summary("Legacy variant endpoint")
|
||||
.description(
|
||||
"**DEPRECATED** - Use `/api/metric/{metric}/{index}` instead.\n\n\
|
||||
Sunset date: 2027-01-01. May be removed earlier in case of abuse.\n\n\
|
||||
Legacy endpoint for querying metrics by variant path (e.g., `day1_to_price`). \
|
||||
Returns raw data without the MetricData wrapper."
|
||||
)
|
||||
.deprecated()
|
||||
.ok_response::<serde_json::Value>()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/vecs/query",
|
||||
get_with(
|
||||
async |uri: Uri,
|
||||
headers: HeaderMap,
|
||||
addr: Extension<SocketAddr>,
|
||||
Query(params): Query<MetricSelectionLegacy>,
|
||||
state: State<AppState>|
|
||||
-> Response {
|
||||
let params: MetricSelection = params.into();
|
||||
legacy::handler(uri, headers, addr, Query(params), state)
|
||||
.await
|
||||
.into_response()
|
||||
},
|
||||
|op| op
|
||||
.metrics_tag()
|
||||
.summary("Legacy query endpoint")
|
||||
.description(
|
||||
"**DEPRECATED** - Use `/api/metric/{metric}/{index}` or `/api/metrics/bulk` instead.\n\n\
|
||||
Sunset date: 2027-01-01. May be removed earlier in case of abuse.\n\n\
|
||||
Legacy endpoint for querying metrics. Returns raw data without the MetricData wrapper."
|
||||
)
|
||||
.deprecated()
|
||||
.ok_response::<serde_json::Value>()
|
||||
.not_modified(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use aide::axum::{ApiRouter, routing::get_with};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderMap, Uri},
|
||||
response::Redirect,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::get,
|
||||
};
|
||||
use brk_types::{
|
||||
@@ -11,7 +11,7 @@ use brk_types::{
|
||||
RewardStats, TimePeriodParam,
|
||||
};
|
||||
|
||||
use crate::{CacheStrategy, extended::TransformResponseExtended};
|
||||
use crate::{CacheStrategy, Error, extended::TransformResponseExtended};
|
||||
|
||||
use super::AppState;
|
||||
|
||||
@@ -200,18 +200,15 @@ impl MiningRoutes for ApiRouter<AppState> {
|
||||
.api_route(
|
||||
"/api/v1/mining/blocks/fee-rates/{time_period}",
|
||||
get_with(
|
||||
async |Path(_path): Path<TimePeriodParam>| {
|
||||
axum::Json(serde_json::json!({
|
||||
"status": "wip",
|
||||
"message": "This endpoint is work in progress. Percentile fields are not yet available."
|
||||
}))
|
||||
async |Path(_path): Path<TimePeriodParam>| -> Response {
|
||||
Error::not_implemented("Fee rate percentiles are not yet available").into_response()
|
||||
},
|
||||
|op| {
|
||||
op.id("get_block_fee_rates")
|
||||
.mining_tag()
|
||||
.summary("Block fee rates (WIP)")
|
||||
.description("**Work in progress.** Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)*")
|
||||
.ok_response::<serde_json::Value>()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ use axum::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Error,
|
||||
api::{
|
||||
addresses::AddressRoutes, blocks::BlockRoutes, mempool::MempoolRoutes,
|
||||
metrics::ApiMetricsRoutes, mining::MiningRoutes, server::ServerRoutes,
|
||||
@@ -39,13 +40,13 @@ pub trait ApiRoutes {
|
||||
|
||||
impl ApiRoutes for ApiRouter<AppState> {
|
||||
fn add_api_routes(self) -> Self {
|
||||
self.add_addresses_routes()
|
||||
self.add_server_routes()
|
||||
.add_metrics_routes()
|
||||
.add_block_routes()
|
||||
.add_tx_routes()
|
||||
.add_addresses_routes()
|
||||
.add_mempool_routes()
|
||||
.add_mining_routes()
|
||||
.add_tx_routes()
|
||||
.add_metrics_routes()
|
||||
.add_server_routes()
|
||||
.route("/api/server", get(Redirect::temporary("/api#tag/server")))
|
||||
.api_route(
|
||||
"/openapi.json",
|
||||
@@ -99,7 +100,9 @@ impl ApiRoutes for ApiRouter<AppState> {
|
||||
)
|
||||
.route(
|
||||
"/api/{*path}",
|
||||
get(|| async { Redirect::permanent("/api") }),
|
||||
get(|| async {
|
||||
Error::not_found("Unknown API endpoint. See /api for documentation.")
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ mod compact;
|
||||
|
||||
pub use compact::ApiJson;
|
||||
|
||||
use aide::openapi::{Contact, Info, License, OpenApi, Server, ServerVariable, Tag};
|
||||
use aide::openapi::{Contact, Info, License, OpenApi, Tag};
|
||||
|
||||
use crate::VERSION;
|
||||
|
||||
@@ -159,38 +159,9 @@ All errors return structured JSON with a consistent format:
|
||||
},
|
||||
];
|
||||
|
||||
let servers = vec![Server {
|
||||
url: "{scheme}://{host}".into(),
|
||||
description: Some("BRK server".into()),
|
||||
variables: [
|
||||
(
|
||||
"scheme".into(),
|
||||
ServerVariable {
|
||||
enumeration: vec!["https".into(), "http".into()],
|
||||
default: "https".into(),
|
||||
description: Some("Protocol".into()),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
(
|
||||
"host".into(),
|
||||
ServerVariable {
|
||||
default: "bitview.space".into(),
|
||||
description: Some(
|
||||
"Server address (e.g. bitview.space or localhost:3110)".into(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
]
|
||||
.into(),
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
OpenApi {
|
||||
info,
|
||||
tags,
|
||||
servers,
|
||||
..OpenApi::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,55 @@ pub trait ServerRoutes {
|
||||
impl ServerRoutes for ApiRouter<AppState> {
|
||||
fn add_server_routes(self) -> Self {
|
||||
self.api_route(
|
||||
"/health",
|
||||
get_with(
|
||||
async |State(state): State<AppState>| -> axum::Json<Health> {
|
||||
let uptime = state.started_instant.elapsed();
|
||||
let tip_height = state.client.get_last_height();
|
||||
let sync = state.sync(|q| {
|
||||
let tip_height = tip_height.unwrap_or(q.indexed_height());
|
||||
q.sync_status(tip_height)
|
||||
});
|
||||
axum::Json(Health {
|
||||
status: Cow::Borrowed("healthy"),
|
||||
service: Cow::Borrowed("brk"),
|
||||
version: Cow::Borrowed(VERSION),
|
||||
timestamp: jiff::Timestamp::now().to_string(),
|
||||
started_at: state.started_at.to_string(),
|
||||
uptime_seconds: uptime.as_secs(),
|
||||
sync,
|
||||
})
|
||||
},
|
||||
|op| {
|
||||
op.id("get_health")
|
||||
.server_tag()
|
||||
.summary("Health check")
|
||||
.description("Returns the health status of the API server, including uptime information.")
|
||||
.ok_response::<Health>()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/version",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
state
|
||||
.cached_json(&headers, CacheStrategy::Static, &uri, |_| {
|
||||
Ok(env!("CARGO_PKG_VERSION"))
|
||||
})
|
||||
.await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_version")
|
||||
.server_tag()
|
||||
.summary("API version")
|
||||
.description("Returns the current version of the API server")
|
||||
.ok_response::<String>()
|
||||
.not_modified()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/api/server/sync",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
@@ -67,55 +116,6 @@ impl ServerRoutes for ApiRouter<AppState> {
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/health",
|
||||
get_with(
|
||||
async |State(state): State<AppState>| -> axum::Json<Health> {
|
||||
let uptime = state.started_instant.elapsed();
|
||||
let tip_height = state.client.get_last_height();
|
||||
let sync = state.sync(|q| {
|
||||
let tip_height = tip_height.unwrap_or(q.indexed_height());
|
||||
q.sync_status(tip_height)
|
||||
});
|
||||
axum::Json(Health {
|
||||
status: Cow::Borrowed("healthy"),
|
||||
service: Cow::Borrowed("brk"),
|
||||
version: Cow::Borrowed(VERSION),
|
||||
timestamp: jiff::Timestamp::now().to_string(),
|
||||
started_at: state.started_at.to_string(),
|
||||
uptime_seconds: uptime.as_secs(),
|
||||
sync,
|
||||
})
|
||||
},
|
||||
|op| {
|
||||
op.id("get_health")
|
||||
.server_tag()
|
||||
.summary("Health check")
|
||||
.description("Returns the health status of the API server, including uptime information.")
|
||||
.ok_response::<Health>()
|
||||
},
|
||||
),
|
||||
)
|
||||
.api_route(
|
||||
"/version",
|
||||
get_with(
|
||||
async |uri: Uri, headers: HeaderMap, State(state): State<AppState>| {
|
||||
state
|
||||
.cached_json(&headers, CacheStrategy::Static, &uri, |_| {
|
||||
Ok(env!("CARGO_PKG_VERSION"))
|
||||
})
|
||||
.await
|
||||
},
|
||||
|op| {
|
||||
op.id("get_version")
|
||||
.server_tag()
|
||||
.summary("API version")
|
||||
.description("Returns the current version of the API server")
|
||||
.ok_response::<String>()
|
||||
.not_modified()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ fn error_status(e: &BrkError) -> StatusCode {
|
||||
| BrkError::UnknownTxid
|
||||
| BrkError::NotFound(_)
|
||||
| BrkError::NoData
|
||||
| BrkError::OutOfRange(_)
|
||||
| BrkError::MetricNotFound(_) => StatusCode::NOT_FOUND,
|
||||
|
||||
BrkError::AuthFailed => StatusCode::FORBIDDEN,
|
||||
@@ -80,6 +81,7 @@ fn error_code(e: &BrkError) -> &'static str {
|
||||
BrkError::UnknownAddress => "unknown_address",
|
||||
BrkError::UnknownTxid => "unknown_txid",
|
||||
BrkError::NotFound(_) => "not_found",
|
||||
BrkError::OutOfRange(_) => "out_of_range",
|
||||
BrkError::NoData => "no_data",
|
||||
BrkError::MetricNotFound(_) => "metric_not_found",
|
||||
BrkError::MempoolNotAvailable => "mempool_not_available",
|
||||
@@ -128,6 +130,10 @@ impl Error {
|
||||
Self::new(StatusCode::NOT_FOUND, "not_found", msg)
|
||||
}
|
||||
|
||||
pub fn not_implemented(msg: impl Into<String>) -> Self {
|
||||
Self::new(StatusCode::NOT_IMPLEMENTED, "not_implemented", msg)
|
||||
}
|
||||
|
||||
pub fn internal(msg: impl Into<String>) -> Self {
|
||||
Self::new(StatusCode::INTERNAL_SERVER_ERROR, "internal_error", msg)
|
||||
}
|
||||
@@ -156,7 +162,7 @@ impl IntoResponse for Error {
|
||||
let body = build_error_body(self.status, self.code, self.message);
|
||||
(
|
||||
self.status,
|
||||
[(header::CONTENT_TYPE, "application/json")],
|
||||
[(header::CONTENT_TYPE, "application/problem+json")],
|
||||
body,
|
||||
)
|
||||
.into_response()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
http::{header, HeaderMap, HeaderValue},
|
||||
http::{HeaderMap, HeaderValue, header},
|
||||
};
|
||||
|
||||
/// HTTP content encoding for pre-compressed caching.
|
||||
@@ -61,9 +61,9 @@ impl ContentEncoding {
|
||||
encoder.write_all(&bytes).expect("gzip compression failed");
|
||||
Bytes::from(encoder.finish().expect("gzip finish failed"))
|
||||
}
|
||||
Self::Zstd => Bytes::from(
|
||||
zstd::encode_all(bytes.as_ref(), 3).expect("zstd compression failed"),
|
||||
),
|
||||
Self::Zstd => {
|
||||
Bytes::from(zstd::encode_all(bytes.as_ref(), 3).expect("zstd compression failed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,12 +24,19 @@ pub trait HeaderMapExtended {
|
||||
|
||||
impl HeaderMapExtended for HeaderMap {
|
||||
fn has_etag(&self, etag: &str) -> bool {
|
||||
self.get(IF_NONE_MATCH)
|
||||
.is_some_and(|prev_etag| etag == prev_etag)
|
||||
self.get(IF_NONE_MATCH).is_some_and(|v| {
|
||||
let s = v.as_bytes();
|
||||
// Match both quoted and unquoted: "etag" or etag
|
||||
s == etag.as_bytes()
|
||||
|| (s.len() == etag.len() + 2
|
||||
&& s[0] == b'"'
|
||||
&& s[s.len() - 1] == b'"'
|
||||
&& &s[1..s.len() - 1] == etag.as_bytes())
|
||||
})
|
||||
}
|
||||
|
||||
fn insert_etag(&mut self, etag: &str) {
|
||||
self.insert(header::ETAG, etag.parse().unwrap());
|
||||
self.insert(header::ETAG, format!("\"{etag}\"").parse().unwrap());
|
||||
}
|
||||
|
||||
fn insert_cache_control(&mut self, value: &str) {
|
||||
@@ -43,7 +50,9 @@ impl HeaderMapExtended for HeaderMap {
|
||||
fn insert_content_disposition_attachment(&mut self, filename: &str) {
|
||||
self.insert(
|
||||
header::CONTENT_DISPOSITION,
|
||||
format!("attachment; filename=\"{filename}\"").parse().unwrap(),
|
||||
format!("attachment; filename=\"{filename}\"")
|
||||
.parse()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
use std::{
|
||||
any::Any,
|
||||
net::SocketAddr,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
@@ -9,11 +10,14 @@ use std::{
|
||||
|
||||
use aide::axum::ApiRouter;
|
||||
use axum::{
|
||||
Extension,
|
||||
Extension, ServiceExt,
|
||||
body::Body,
|
||||
http::{Request, Response, StatusCode, Uri},
|
||||
http::{
|
||||
Request, Response, StatusCode, Uri,
|
||||
header::{CONTENT_TYPE, VARY},
|
||||
},
|
||||
middleware::Next,
|
||||
response::Redirect,
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::get,
|
||||
serve,
|
||||
};
|
||||
@@ -25,6 +29,7 @@ use tower_http::{
|
||||
compression::CompressionLayer, cors::CorsLayer, normalize_path::NormalizePathLayer,
|
||||
timeout::TimeoutLayer, trace::TraceLayer,
|
||||
};
|
||||
use tower_layer::Layer;
|
||||
use tracing::{error, info};
|
||||
|
||||
mod api;
|
||||
@@ -101,32 +106,57 @@ impl Server {
|
||||
},
|
||||
);
|
||||
|
||||
// Wrap non-JSON client errors (e.g. axum extraction rejections) in structured JSON
|
||||
// Wrap non-JSON error responses in structured JSON
|
||||
let json_error_layer = axum::middleware::from_fn(
|
||||
async |request: Request<Body>, next: Next| -> Response<Body> {
|
||||
use axum::http::header::CONTENT_TYPE;
|
||||
use axum::response::IntoResponse;
|
||||
|
||||
let response = next.run(request).await;
|
||||
if !response.status().is_client_error()
|
||||
|| response
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.is_some_and(|v| v.as_bytes().starts_with(b"application/json"))
|
||||
let status = response.status();
|
||||
if status.is_success()
|
||||
|| status.is_redirection()
|
||||
|| status.is_informational()
|
||||
|| response.headers().get(CONTENT_TYPE).is_some_and(|v| {
|
||||
let b = v.as_bytes();
|
||||
b.starts_with(b"application/") && b.ends_with(b"json")
|
||||
})
|
||||
{
|
||||
return response;
|
||||
}
|
||||
|
||||
let (parts, body) = response.into_parts();
|
||||
let bytes = axum::body::to_bytes(body, 4096)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let msg = String::from_utf8_lossy(&bytes).to_string();
|
||||
let code = if parts.status == StatusCode::NOT_FOUND {
|
||||
"not_found"
|
||||
} else {
|
||||
"bad_request"
|
||||
let bytes = axum::body::to_bytes(body, 4096).await.unwrap_or_default();
|
||||
let msg = String::from_utf8_lossy(&bytes);
|
||||
let (code, msg) = match parts.status {
|
||||
StatusCode::NOT_FOUND => (
|
||||
"not_found",
|
||||
if msg.is_empty() {
|
||||
"Not found".into()
|
||||
} else {
|
||||
msg
|
||||
},
|
||||
),
|
||||
StatusCode::METHOD_NOT_ALLOWED => (
|
||||
"method_not_allowed",
|
||||
"Only GET requests are supported".into(),
|
||||
),
|
||||
StatusCode::GATEWAY_TIMEOUT => ("timeout", "Request timed out".into()),
|
||||
s if s.is_client_error() => (
|
||||
"bad_request",
|
||||
if msg.is_empty() {
|
||||
"Bad request".into()
|
||||
} else {
|
||||
msg
|
||||
},
|
||||
),
|
||||
_ => (
|
||||
"internal_error",
|
||||
if msg.is_empty() {
|
||||
"Internal server error".into()
|
||||
} else {
|
||||
msg
|
||||
},
|
||||
),
|
||||
};
|
||||
let msg = msg.into_owned();
|
||||
let mut response = Error::new(parts.status, code, msg).into_response();
|
||||
response.extensions_mut().extend(parts.extensions);
|
||||
response
|
||||
@@ -165,18 +195,41 @@ impl Server {
|
||||
let router = router
|
||||
.with_state(state)
|
||||
.merge(website_router)
|
||||
.layer(json_error_layer)
|
||||
.layer(compression_layer)
|
||||
.layer(response_time_layer)
|
||||
.layer(trace_layer)
|
||||
.layer(CatchPanicLayer::new())
|
||||
.layer(CatchPanicLayer::custom(|panic: Box<dyn Any + Send>| {
|
||||
let msg = panic
|
||||
.downcast_ref::<String>()
|
||||
.map(|s| s.as_str())
|
||||
.or_else(|| panic.downcast_ref::<&str>().copied())
|
||||
.unwrap_or("Unknown panic");
|
||||
Error::internal(msg).into_response()
|
||||
}))
|
||||
.layer(TimeoutLayer::with_status_code(
|
||||
StatusCode::GATEWAY_TIMEOUT,
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.layer(json_error_layer)
|
||||
.layer(CorsLayer::permissive())
|
||||
.layer(connect_info_layer)
|
||||
.layer(NormalizePathLayer::trim_trailing_slash());
|
||||
.layer(axum::middleware::from_fn(
|
||||
async |request: Request<Body>, next: Next| -> Response<Body> {
|
||||
let mut response = next.run(request).await;
|
||||
// Consolidate multiple Vary headers into one
|
||||
let vary: Vec<&str> = response
|
||||
.headers()
|
||||
.get_all(VARY)
|
||||
.iter()
|
||||
.filter_map(|v| v.to_str().ok())
|
||||
.collect();
|
||||
if vary.len() > 1 {
|
||||
let merged = vary.join(", ");
|
||||
response.headers_mut().insert(VARY, merged.parse().unwrap());
|
||||
}
|
||||
response
|
||||
},
|
||||
))
|
||||
.layer(connect_info_layer);
|
||||
|
||||
let (listener, port) = match port {
|
||||
Some(port) => {
|
||||
@@ -235,9 +288,12 @@ impl Server {
|
||||
.layer(Extension(Arc::new(openapi)))
|
||||
.layer(Extension(api_json));
|
||||
|
||||
// NormalizePath must wrap the router (not be a layer) to run before route matching
|
||||
let app = NormalizePathLayer::trim_trailing_slash().layer(router);
|
||||
|
||||
serve(
|
||||
listener,
|
||||
router.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
ServiceExt::<Request<Body>>::into_make_service_with_connect_info::<SocketAddr>(app),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -60,9 +60,10 @@ impl AppState {
|
||||
|
||||
let full_key = format!("{}-{}-{}", uri, params.etag_str(), encoding.as_str());
|
||||
let result = self
|
||||
.get_or_insert(&full_key, async move {
|
||||
self.run(move |q| f(q, encoding)).await
|
||||
})
|
||||
.get_or_insert(
|
||||
&full_key,
|
||||
async move { self.run(move |q| f(q, encoding)).await },
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
|
||||
@@ -43,10 +43,18 @@ impl TryFrom<&ScriptBuf> for Address {
|
||||
impl TryFrom<(&ScriptBuf, OutputType)> for Address {
|
||||
type Error = Error;
|
||||
fn try_from((script, outputtype): (&ScriptBuf, OutputType)) -> Result<Self, Self::Error> {
|
||||
if outputtype.is_address() {
|
||||
Ok(Self(script.to_hex_string()))
|
||||
} else {
|
||||
Err(Error::InvalidAddress)
|
||||
match outputtype {
|
||||
OutputType::P2PK65 | OutputType::P2PK33 => {
|
||||
// P2PK has no standard address encoding, use raw pubkey hex
|
||||
let bytes = AddressBytes::try_from((script, outputtype))?;
|
||||
Ok(Self(bytes_to_hex(bytes.as_slice())))
|
||||
}
|
||||
_ if outputtype.is_address() => {
|
||||
let addr = bitcoin::Address::from_script(script, bitcoin::Network::Bitcoin)
|
||||
.map_err(|_| Error::InvalidAddress)?;
|
||||
Ok(Self(addr.to_string()))
|
||||
}
|
||||
_ => Err(Error::InvalidAddress),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,14 +62,7 @@ impl TryFrom<(&ScriptBuf, OutputType)> for Address {
|
||||
impl TryFrom<&AddressBytes> for Address {
|
||||
type Error = Error;
|
||||
fn try_from(bytes: &AddressBytes) -> Result<Self, Self::Error> {
|
||||
// P2PK addresses are represented as raw pubkey hex, not as a script
|
||||
let address = match bytes {
|
||||
AddressBytes::P2PK65(_) | AddressBytes::P2PK33(_) => {
|
||||
Self::from(bytes_to_hex(bytes.as_slice()))
|
||||
}
|
||||
_ => Self::try_from(&bytes.to_script_pubkey())?,
|
||||
};
|
||||
Ok(address)
|
||||
Self::try_from(&bytes.to_script_pubkey())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,8 +50,8 @@ impl From<ByteView> for AddressIndexOutPoint {
|
||||
#[inline]
|
||||
fn from(value: ByteView) -> Self {
|
||||
Self {
|
||||
addressindextxindex: AddressIndexTxIndex::from_bytes(&value[0..8]).unwrap(),
|
||||
vout: Vout::from_bytes(&value[8..]).unwrap(),
|
||||
addressindextxindex: AddressIndexTxIndex::from(ByteView::new(&value[..8])),
|
||||
vout: Vout::from(u16::from_be_bytes([value[8], value[9]])),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,4 @@ use crate::Txid;
|
||||
pub struct AddressTxidsParam {
|
||||
/// Txid to paginate from (return transactions before this one)
|
||||
pub after_txid: Option<Txid>,
|
||||
/// Maximum number of results to return. Defaults to 25 if not specified.
|
||||
#[serde(default = "default_limit")]
|
||||
pub limit: usize,
|
||||
}
|
||||
|
||||
fn default_limit() -> usize {
|
||||
25
|
||||
}
|
||||
|
||||
@@ -83,7 +83,9 @@ impl From<BasisPointsSigned32> for i32 {
|
||||
impl From<f64> for BasisPointsSigned32 {
|
||||
#[inline]
|
||||
fn from(value: f64) -> Self {
|
||||
let scaled = (value * 10000.0).round().clamp(i32::MIN as f64, i32::MAX as f64);
|
||||
let scaled = (value * 10000.0)
|
||||
.round()
|
||||
.clamp(i32::MIN as f64, i32::MAX as f64);
|
||||
Self(scaled as i32)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,18 @@ impl std::fmt::Display for Bitcoin {
|
||||
impl Formattable for Bitcoin {
|
||||
#[inline(always)]
|
||||
fn write_to(&self, buf: &mut Vec<u8>) {
|
||||
let mut b = ryu::Buffer::new();
|
||||
buf.extend_from_slice(b.format(self.0).as_bytes());
|
||||
if !self.0.is_nan() {
|
||||
let mut b = ryu::Buffer::new();
|
||||
buf.extend_from_slice(b.format(self.0).as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn fmt_json(&self, buf: &mut Vec<u8>) {
|
||||
if self.0.is_nan() {
|
||||
buf.extend_from_slice(b"null");
|
||||
} else {
|
||||
self.write_to(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,7 +435,18 @@ impl std::fmt::Display for Dollars {
|
||||
impl Formattable for Dollars {
|
||||
#[inline(always)]
|
||||
fn write_to(&self, buf: &mut Vec<u8>) {
|
||||
let mut b = ryu::Buffer::new();
|
||||
buf.extend_from_slice(b.format(self.0).as_bytes());
|
||||
if !self.0.is_nan() {
|
||||
let mut b = ryu::Buffer::new();
|
||||
buf.extend_from_slice(b.format(self.0).as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn fmt_json(&self, buf: &mut Vec<u8>) {
|
||||
if self.0.is_nan() {
|
||||
buf.extend_from_slice(b"null");
|
||||
} else {
|
||||
self.write_to(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ impl PrintableIndex for Hour1 {
|
||||
}
|
||||
|
||||
fn to_possible_strings() -> &'static [&'static str] {
|
||||
&["1h", "h", "hour", "hourly", "hour1"]
|
||||
&["1h", "hour", "hourly", "hour1"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ use vecdb::PrintableIndex;
|
||||
use crate::PairOutputIndex;
|
||||
|
||||
use super::{
|
||||
Date, Day1, Day3, Epoch, EmptyAddressIndex, EmptyOutputIndex, FundedAddressIndex,
|
||||
Halving, Height, Hour1, Hour4, Hour12, Minute10, Minute30, Month1, Month3, Month6,
|
||||
OpReturnIndex, P2AAddressIndex, P2MSOutputIndex, P2PK33AddressIndex, P2PK65AddressIndex,
|
||||
P2PKHAddressIndex, P2SHAddressIndex, P2TRAddressIndex, P2WPKHAddressIndex, P2WSHAddressIndex,
|
||||
Timestamp, TxInIndex, TxIndex, TxOutIndex, UnknownOutputIndex, Week1, Year1, Year10,
|
||||
Date, Day1, Day3, EmptyAddressIndex, EmptyOutputIndex, Epoch, FundedAddressIndex, Halving,
|
||||
Height, Hour1, Hour4, Hour12, Minute10, Minute30, Month1, Month3, Month6, OpReturnIndex,
|
||||
P2AAddressIndex, P2MSOutputIndex, P2PK33AddressIndex, P2PK65AddressIndex, P2PKHAddressIndex,
|
||||
P2SHAddressIndex, P2TRAddressIndex, P2WPKHAddressIndex, P2WSHAddressIndex, Timestamp,
|
||||
TxInIndex, TxIndex, TxOutIndex, UnknownOutputIndex, Week1, Year1, Year10,
|
||||
hour1::HOUR1_INTERVAL, hour4::HOUR4_INTERVAL, hour12::HOUR12_INTERVAL,
|
||||
minute10::MINUTE10_INTERVAL, minute30::MINUTE30_INTERVAL, timestamp::INDEX_EPOCH,
|
||||
};
|
||||
@@ -184,7 +184,6 @@ impl Index {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Returns true if this index type is date-based.
|
||||
pub const fn is_date_based(&self) -> bool {
|
||||
matches!(
|
||||
|
||||
@@ -57,12 +57,12 @@ mod deser;
|
||||
mod difficultyadjustment;
|
||||
mod difficultyadjustmententry;
|
||||
mod difficultyentry;
|
||||
mod epoch;
|
||||
mod diskusage;
|
||||
mod dollars;
|
||||
mod emptyaddressdata;
|
||||
mod emptyaddressindex;
|
||||
mod emptyoutputindex;
|
||||
mod epoch;
|
||||
mod etag;
|
||||
mod feerate;
|
||||
mod feeratepercentiles;
|
||||
@@ -149,8 +149,8 @@ mod recommendedfees;
|
||||
mod rewardstats;
|
||||
mod sats;
|
||||
mod sats_signed;
|
||||
mod searchquery;
|
||||
mod satsfract;
|
||||
mod searchquery;
|
||||
mod stored_bool;
|
||||
mod stored_f32;
|
||||
mod stored_f64;
|
||||
@@ -253,12 +253,12 @@ pub use deser::*;
|
||||
pub use difficultyadjustment::*;
|
||||
pub use difficultyadjustmententry::*;
|
||||
pub use difficultyentry::*;
|
||||
pub use epoch::*;
|
||||
pub use diskusage::*;
|
||||
pub use dollars::*;
|
||||
pub use emptyaddressdata::*;
|
||||
pub use emptyaddressindex::*;
|
||||
pub use emptyoutputindex::*;
|
||||
pub use epoch::*;
|
||||
pub use etag::*;
|
||||
pub use feerate::*;
|
||||
pub use feeratepercentiles::*;
|
||||
|
||||
@@ -14,6 +14,10 @@ pub struct Limit(usize);
|
||||
impl Limit {
|
||||
pub const MIN: Self = Self(1);
|
||||
pub const DEFAULT: Self = Self(100);
|
||||
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.0 == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Limit {
|
||||
|
||||
@@ -12,6 +12,12 @@ pub struct PaginatedMetrics {
|
||||
/// Maximum valid page index (0-indexed)
|
||||
#[schemars(example = 21)]
|
||||
pub max_page: usize,
|
||||
/// List of metric names (max 1000 per page)
|
||||
/// Total number of metrics
|
||||
pub total_count: usize,
|
||||
/// Results per page
|
||||
pub per_page: usize,
|
||||
/// Whether more pages are available after the current one
|
||||
pub has_more: bool,
|
||||
/// List of metric names
|
||||
pub metrics: Vec<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
@@ -129,10 +129,15 @@ impl Display for OHLCCents {
|
||||
|
||||
impl Formattable for OHLCCents {
|
||||
fn write_to(&self, buf: &mut Vec<u8>) {
|
||||
use std::fmt::Write;
|
||||
let mut s = String::new();
|
||||
write!(s, "{}", self).unwrap();
|
||||
buf.extend_from_slice(s.as_bytes());
|
||||
buf.push(b'[');
|
||||
self.open.write_to(buf);
|
||||
buf.push(b',');
|
||||
self.high.write_to(buf);
|
||||
buf.push(b',');
|
||||
self.low.write_to(buf);
|
||||
buf.push(b',');
|
||||
self.close.write_to(buf);
|
||||
buf.push(b']');
|
||||
}
|
||||
|
||||
fn fmt_csv(&self, f: &mut String) -> std::fmt::Result {
|
||||
@@ -144,12 +149,6 @@ impl Formattable for OHLCCents {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fmt_json(&self, buf: &mut Vec<u8>) {
|
||||
buf.push(b'"');
|
||||
self.write_to(buf);
|
||||
buf.push(b'"');
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytes for OHLCCents {
|
||||
@@ -272,10 +271,15 @@ impl Display for OHLCDollars {
|
||||
|
||||
impl Formattable for OHLCDollars {
|
||||
fn write_to(&self, buf: &mut Vec<u8>) {
|
||||
use std::fmt::Write;
|
||||
let mut s = String::new();
|
||||
write!(s, "{}", self).unwrap();
|
||||
buf.extend_from_slice(s.as_bytes());
|
||||
buf.push(b'[');
|
||||
self.open.write_to(buf);
|
||||
buf.push(b',');
|
||||
self.high.write_to(buf);
|
||||
buf.push(b',');
|
||||
self.low.write_to(buf);
|
||||
buf.push(b',');
|
||||
self.close.write_to(buf);
|
||||
buf.push(b']');
|
||||
}
|
||||
|
||||
fn fmt_csv(&self, f: &mut String) -> std::fmt::Result {
|
||||
@@ -287,12 +291,6 @@ impl Formattable for OHLCDollars {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fmt_json(&self, buf: &mut Vec<u8>) {
|
||||
buf.push(b'"');
|
||||
self.write_to(buf);
|
||||
buf.push(b'"');
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytes for OHLCDollars {
|
||||
@@ -384,10 +382,15 @@ impl Display for OHLCSats {
|
||||
|
||||
impl Formattable for OHLCSats {
|
||||
fn write_to(&self, buf: &mut Vec<u8>) {
|
||||
use std::fmt::Write;
|
||||
let mut s = String::new();
|
||||
write!(s, "{}", self).unwrap();
|
||||
buf.extend_from_slice(s.as_bytes());
|
||||
buf.push(b'[');
|
||||
self.open.write_to(buf);
|
||||
buf.push(b',');
|
||||
self.high.write_to(buf);
|
||||
buf.push(b',');
|
||||
self.low.write_to(buf);
|
||||
buf.push(b',');
|
||||
self.close.write_to(buf);
|
||||
buf.push(b']');
|
||||
}
|
||||
|
||||
fn fmt_csv(&self, f: &mut String) -> std::fmt::Result {
|
||||
@@ -399,12 +402,6 @@ impl Formattable for OHLCSats {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fmt_json(&self, buf: &mut Vec<u8>) {
|
||||
buf.push(b'"');
|
||||
self.write_to(buf);
|
||||
buf.push(b'"');
|
||||
}
|
||||
}
|
||||
|
||||
impl Bytes for OHLCSats {
|
||||
|
||||
@@ -8,20 +8,31 @@ pub struct Pagination {
|
||||
#[serde(default, alias = "p")]
|
||||
#[schemars(example = 0, example = 1, example = 2)]
|
||||
pub page: Option<usize>,
|
||||
/// Results per page (default: 1000, max: 1000)
|
||||
#[serde(default)]
|
||||
#[schemars(example = 100, example = 1000)]
|
||||
pub per_page: Option<usize>,
|
||||
}
|
||||
|
||||
impl Pagination {
|
||||
pub const PER_PAGE: usize = 1_000;
|
||||
pub const DEFAULT_PER_PAGE: usize = 1_000;
|
||||
pub const MAX_PER_PAGE: usize = 1_000;
|
||||
|
||||
pub fn page(&self) -> usize {
|
||||
self.page.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn per_page(&self) -> usize {
|
||||
self.per_page
|
||||
.unwrap_or(Self::DEFAULT_PER_PAGE)
|
||||
.min(Self::MAX_PER_PAGE)
|
||||
}
|
||||
|
||||
pub fn start(&self, len: usize) -> usize {
|
||||
(self.page() * Self::PER_PAGE).clamp(0, len)
|
||||
(self.page() * self.per_page()).clamp(0, len)
|
||||
}
|
||||
|
||||
pub fn end(&self, len: usize) -> usize {
|
||||
((self.page() + 1) * Self::PER_PAGE).clamp(0, len)
|
||||
((self.page() + 1) * self.per_page()).clamp(0, len)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,22 +169,22 @@ pub enum PoolSlug {
|
||||
Ocean,
|
||||
WhitePool,
|
||||
Wiz,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy145,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy146,
|
||||
Wk057,
|
||||
FutureBitApolloSolo,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy149,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy150,
|
||||
CarbonNegative,
|
||||
PortlandHodl,
|
||||
Phoenix,
|
||||
Neopool,
|
||||
MaxiPool,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy156,
|
||||
BitFuFuPool,
|
||||
GDPool,
|
||||
@@ -192,187 +192,187 @@ pub enum PoolSlug {
|
||||
PublicPool,
|
||||
MiningSquared,
|
||||
InnopolisTech,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy163,
|
||||
BtcLab,
|
||||
Parasite,
|
||||
RedRockPool,
|
||||
Est3lar,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy168,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy169,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy170,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy171,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy172,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy173,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy174,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy175,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy176,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy177,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy178,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy179,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy180,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy181,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy182,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy183,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy184,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy185,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy186,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy187,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy188,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy189,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy190,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy191,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy192,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy193,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy194,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy195,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy196,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy197,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy198,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy199,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy200,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy201,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy202,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy203,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy204,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy205,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy206,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy207,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy208,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy209,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy210,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy211,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy212,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy213,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy214,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy215,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy216,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy217,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy218,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy219,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy220,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy221,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy222,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy223,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy224,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy225,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy226,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy227,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy228,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy229,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy230,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy231,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy232,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy233,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy234,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy235,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy236,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy237,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy238,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy239,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy240,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy241,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy242,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy243,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy244,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy245,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy246,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy247,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy248,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy249,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy250,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy251,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy252,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy253,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy254,
|
||||
#[schemars(skip)]
|
||||
#[serde(skip)]
|
||||
Dummy255,
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ impl<I: Default + Copy, V: Default + Copy> Default for RangeMap<I, V> {
|
||||
|
||||
impl<I: Ord + Copy + Default + Into<usize>, V: From<usize> + Copy + Default> RangeMap<I, V> {
|
||||
/// Number of ranges stored.
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.first_indexes.len()
|
||||
}
|
||||
@@ -102,7 +103,12 @@ impl<I: Ord + Copy + Default + Into<usize>, V: From<usize> + Copy + Default> Ran
|
||||
if pos > 0 {
|
||||
let value = V::from(pos - 1);
|
||||
if pos < self.first_indexes.len() {
|
||||
self.cache[slot] = (self.first_indexes[pos - 1], self.first_indexes[pos], value, true);
|
||||
self.cache[slot] = (
|
||||
self.first_indexes[pos - 1],
|
||||
self.first_indexes[pos],
|
||||
value,
|
||||
true,
|
||||
);
|
||||
}
|
||||
Some(value)
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::fmt;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
@@ -12,6 +14,34 @@ pub enum RangeIndex {
|
||||
Timestamp(Timestamp),
|
||||
}
|
||||
|
||||
impl From<i64> for RangeIndex {
|
||||
fn from(i: i64) -> Self {
|
||||
Self::Int(i)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Date> for RangeIndex {
|
||||
fn from(d: Date) -> Self {
|
||||
Self::Date(d)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Timestamp> for RangeIndex {
|
||||
fn from(t: Timestamp) -> Self {
|
||||
Self::Timestamp(t)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RangeIndex {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Int(i) => write!(f, "{i}"),
|
||||
Self::Date(d) => write!(f, "{d}"),
|
||||
Self::Timestamp(t) => write!(f, "{t}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RangeIndex {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
let s = String::deserialize(deserializer)?;
|
||||
|
||||
@@ -189,8 +189,19 @@ impl std::fmt::Display for SatsFract {
|
||||
impl Formattable for SatsFract {
|
||||
#[inline(always)]
|
||||
fn write_to(&self, buf: &mut Vec<u8>) {
|
||||
let mut b = ryu::Buffer::new();
|
||||
buf.extend_from_slice(b.format(self.0).as_bytes());
|
||||
if !self.0.is_nan() {
|
||||
let mut b = ryu::Buffer::new();
|
||||
buf.extend_from_slice(b.format(self.0).as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn fmt_json(&self, buf: &mut Vec<u8>) {
|
||||
if self.0.is_nan() {
|
||||
buf.extend_from_slice(b"null");
|
||||
} else {
|
||||
self.write_to(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -274,7 +274,18 @@ impl std::fmt::Display for StoredF32 {
|
||||
impl Formattable for StoredF32 {
|
||||
#[inline(always)]
|
||||
fn write_to(&self, buf: &mut Vec<u8>) {
|
||||
let mut b = ryu::Buffer::new();
|
||||
buf.extend_from_slice(b.format(self.0).as_bytes());
|
||||
if !self.0.is_nan() {
|
||||
let mut b = ryu::Buffer::new();
|
||||
buf.extend_from_slice(b.format(self.0).as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn fmt_json(&self, buf: &mut Vec<u8>) {
|
||||
if self.0.is_nan() {
|
||||
buf.extend_from_slice(b"null");
|
||||
} else {
|
||||
self.write_to(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,18 @@ impl std::fmt::Display for StoredF64 {
|
||||
impl Formattable for StoredF64 {
|
||||
#[inline(always)]
|
||||
fn write_to(&self, buf: &mut Vec<u8>) {
|
||||
let mut b = ryu::Buffer::new();
|
||||
buf.extend_from_slice(b.format(self.0).as_bytes());
|
||||
if !self.0.is_nan() {
|
||||
let mut b = ryu::Buffer::new();
|
||||
buf.extend_from_slice(b.format(self.0).as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn fmt_json(&self, buf: &mut Vec<u8>) {
|
||||
if self.0.is_nan() {
|
||||
buf.extend_from_slice(b"null");
|
||||
} else {
|
||||
self.write_to(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,10 +487,7 @@ mod tests {
|
||||
"epoch",
|
||||
branch(vec![
|
||||
("sum", leaf("metric_sum", Index::Epoch)),
|
||||
(
|
||||
"cumulative",
|
||||
leaf("metric_cumulative", Index::Epoch),
|
||||
),
|
||||
("cumulative", leaf("metric_cumulative", Index::Epoch)),
|
||||
]),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{TxOut, Txid, Vout};
|
||||
use bitcoin::{Script, ScriptBuf};
|
||||
use bitcoin::ScriptBuf;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct};
|
||||
|
||||
@@ -60,8 +60,11 @@ impl TxIn {
|
||||
self.script_sig.to_asm_string()
|
||||
}
|
||||
|
||||
pub fn redeem_script(&self) -> Option<&Script> {
|
||||
self.script_sig.redeem_script()
|
||||
pub fn inner_redeemscript_asm(&self) -> String {
|
||||
self.script_sig
|
||||
.redeem_script()
|
||||
.map(|s| s.to_asm_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +82,7 @@ impl Serialize for TxIn {
|
||||
state.serialize_field("scriptsig_asm", &self.script_sig_asm())?;
|
||||
state.serialize_field("is_coinbase", &self.is_coinbase)?;
|
||||
state.serialize_field("sequence", &self.sequence)?;
|
||||
state.serialize_field("inner_redeemscript_asm", &self.redeem_script())?;
|
||||
state.serialize_field("inner_redeemscript_asm", &self.inner_redeemscript_asm())?;
|
||||
|
||||
state.end()
|
||||
}
|
||||
|
||||
@@ -40,6 +40,13 @@ impl Vout {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Vout {
|
||||
#[inline]
|
||||
fn from(value: u16) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
const U16_MAX_AS_U32: u32 = u16::MAX as u32;
|
||||
impl From<u32> for Vout {
|
||||
#[inline]
|
||||
|
||||
+26
-25
@@ -48,7 +48,6 @@
|
||||
/**
|
||||
* @typedef {Object} AddressTxidsParam
|
||||
* @property {(Txid|null)=} afterTxid - Txid to paginate from (return transactions before this one)
|
||||
* @property {number=} limit - Maximum number of results to return. Defaults to 25 if not specified.
|
||||
*/
|
||||
/**
|
||||
* Address validation result
|
||||
@@ -603,13 +602,17 @@
|
||||
* @typedef {Object} PaginatedMetrics
|
||||
* @property {number} currentPage - Current page number (0-indexed)
|
||||
* @property {number} maxPage - Maximum valid page index (0-indexed)
|
||||
* @property {string[]} metrics - List of metric names (max 1000 per page)
|
||||
* @property {number} totalCount - Total number of metrics
|
||||
* @property {number} perPage - Results per page
|
||||
* @property {boolean} hasMore - Whether more pages are available after the current one
|
||||
* @property {string[]} metrics - List of metric names
|
||||
*/
|
||||
/**
|
||||
* Pagination parameters for paginated API endpoints
|
||||
*
|
||||
* @typedef {Object} Pagination
|
||||
* @property {?number=} page - Pagination index
|
||||
* @property {?number=} perPage - Results per page (default: 1000, max: 1000)
|
||||
*/
|
||||
/**
|
||||
* Block counts for different time periods
|
||||
@@ -8732,23 +8735,21 @@ class BrkClient extends BrkClientBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Address transaction IDs
|
||||
* Address transactions
|
||||
*
|
||||
* Get transaction IDs for an address, newest first. Use after_txid for pagination.
|
||||
* Get transaction history for an address, sorted with newest first. Returns up to 50 mempool transactions plus the first 25 confirmed transactions. Use ?after_txid=<txid> for pagination.
|
||||
*
|
||||
* *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*
|
||||
*
|
||||
* Endpoint: `GET /api/address/{address}/txs`
|
||||
*
|
||||
* @param {Address} address
|
||||
* @param {string=} [after_txid] - Txid to paginate from (return transactions before this one)
|
||||
* @param {number=} [limit] - Maximum number of results to return. Defaults to 25 if not specified.
|
||||
* @returns {Promise<Txid[]>}
|
||||
* @param {Txid=} [after_txid] - Txid to paginate from (return transactions before this one)
|
||||
* @returns {Promise<Transaction[]>}
|
||||
*/
|
||||
async getAddressTxs(address, after_txid, limit) {
|
||||
async getAddressTxs(address, after_txid) {
|
||||
const params = new URLSearchParams();
|
||||
if (after_txid !== undefined) params.set('after_txid', String(after_txid));
|
||||
if (limit !== undefined) params.set('limit', String(limit));
|
||||
const query = params.toString();
|
||||
const path = `/api/address/${address}/txs${query ? '?' + query : ''}`;
|
||||
return this.getJson(path);
|
||||
@@ -8757,21 +8758,19 @@ class BrkClient extends BrkClientBase {
|
||||
/**
|
||||
* Address confirmed transactions
|
||||
*
|
||||
* Get confirmed transaction IDs for an address, 25 per page. Use ?after_txid=<txid> for pagination.
|
||||
* Get confirmed transactions for an address, 25 per page. Use ?after_txid=<txid> for pagination.
|
||||
*
|
||||
* *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)*
|
||||
*
|
||||
* Endpoint: `GET /api/address/{address}/txs/chain`
|
||||
*
|
||||
* @param {Address} address
|
||||
* @param {string=} [after_txid] - Txid to paginate from (return transactions before this one)
|
||||
* @param {number=} [limit] - Maximum number of results to return. Defaults to 25 if not specified.
|
||||
* @returns {Promise<Txid[]>}
|
||||
* @param {Txid=} [after_txid] - Txid to paginate from (return transactions before this one)
|
||||
* @returns {Promise<Transaction[]>}
|
||||
*/
|
||||
async getAddressConfirmedTxs(address, after_txid, limit) {
|
||||
async getAddressConfirmedTxs(address, after_txid) {
|
||||
const params = new URLSearchParams();
|
||||
if (after_txid !== undefined) params.set('after_txid', String(after_txid));
|
||||
if (limit !== undefined) params.set('limit', String(limit));
|
||||
const query = params.toString();
|
||||
const path = `/api/address/${address}/txs/chain${query ? '?' + query : ''}`;
|
||||
return this.getJson(path);
|
||||
@@ -9016,9 +9015,9 @@ class BrkClient extends BrkClientBase {
|
||||
*
|
||||
* @param {Metric} metric - Metric name
|
||||
* @param {Index} index - Aggregation index
|
||||
* @param {string=} [start] - Inclusive start: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `from`, `f`, `s`
|
||||
* @param {string=} [end] - Exclusive end: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `to`, `t`, `e`
|
||||
* @param {string=} [limit] - Maximum number of values to return (ignored if `end` is set). Aliases: `count`, `c`, `l`
|
||||
* @param {RangeIndex=} [start] - Inclusive start: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `from`, `f`, `s`
|
||||
* @param {RangeIndex=} [end] - Exclusive end: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `to`, `t`, `e`
|
||||
* @param {Limit=} [limit] - Maximum number of values to return (ignored if `end` is set). Aliases: `count`, `c`, `l`
|
||||
* @param {Format=} [format] - Format of the output
|
||||
* @returns {Promise<AnyMetricData | string>}
|
||||
*/
|
||||
@@ -9045,9 +9044,9 @@ class BrkClient extends BrkClientBase {
|
||||
*
|
||||
* @param {Metric} metric - Metric name
|
||||
* @param {Index} index - Aggregation index
|
||||
* @param {string=} [start] - Inclusive start: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `from`, `f`, `s`
|
||||
* @param {string=} [end] - Exclusive end: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `to`, `t`, `e`
|
||||
* @param {string=} [limit] - Maximum number of values to return (ignored if `end` is set). Aliases: `count`, `c`, `l`
|
||||
* @param {RangeIndex=} [start] - Inclusive start: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `from`, `f`, `s`
|
||||
* @param {RangeIndex=} [end] - Exclusive end: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `to`, `t`, `e`
|
||||
* @param {Limit=} [limit] - Maximum number of values to return (ignored if `end` is set). Aliases: `count`, `c`, `l`
|
||||
* @param {Format=} [format] - Format of the output
|
||||
* @returns {Promise<boolean[] | string>}
|
||||
*/
|
||||
@@ -9131,9 +9130,9 @@ class BrkClient extends BrkClientBase {
|
||||
*
|
||||
* @param {Metrics} [metrics] - Requested metrics
|
||||
* @param {Index} [index] - Index to query
|
||||
* @param {string=} [start] - Inclusive start: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `from`, `f`, `s`
|
||||
* @param {string=} [end] - Exclusive end: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `to`, `t`, `e`
|
||||
* @param {string=} [limit] - Maximum number of values to return (ignored if `end` is set). Aliases: `count`, `c`, `l`
|
||||
* @param {RangeIndex=} [start] - Inclusive start: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `from`, `f`, `s`
|
||||
* @param {RangeIndex=} [end] - Exclusive end: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `to`, `t`, `e`
|
||||
* @param {Limit=} [limit] - Maximum number of values to return (ignored if `end` is set). Aliases: `count`, `c`, `l`
|
||||
* @param {Format=} [format] - Format of the output
|
||||
* @returns {Promise<AnyMetricData[] | string>}
|
||||
*/
|
||||
@@ -9237,11 +9236,13 @@ class BrkClient extends BrkClientBase {
|
||||
* Endpoint: `GET /api/metrics/list`
|
||||
*
|
||||
* @param {number=} [page] - Pagination index
|
||||
* @param {number=} [per_page] - Results per page (default: 1000, max: 1000)
|
||||
* @returns {Promise<PaginatedMetrics>}
|
||||
*/
|
||||
async listMetrics(page) {
|
||||
async listMetrics(page, per_page) {
|
||||
const params = new URLSearchParams();
|
||||
if (page !== undefined) params.set('page', String(page));
|
||||
if (per_page !== undefined) params.set('per_page', String(per_page));
|
||||
const query = params.toString();
|
||||
const path = `/api/metrics/list${query ? '?' + query : ''}`;
|
||||
return this.getJson(path);
|
||||
|
||||
@@ -68,7 +68,9 @@ console.log("\n7. dateEntries():");
|
||||
const dateEntries = price.dateEntries();
|
||||
if (!(dateEntries[0][0] instanceof Date))
|
||||
throw new Error("Expected Date entry key");
|
||||
console.log(` First: [${dateEntries[0][0].toISOString()}, ${dateEntries[0][1]}]`);
|
||||
console.log(
|
||||
` First: [${dateEntries[0][0].toISOString()}, ${dateEntries[0][1]}]`,
|
||||
);
|
||||
|
||||
// Test toMap() - returns Map<number, value>
|
||||
console.log("\n8. toMap():");
|
||||
@@ -95,7 +97,7 @@ if (count !== 5) throw new Error("Expected 5 iterations");
|
||||
|
||||
// Test with non-date-based index (height)
|
||||
console.log("\n11. Testing height-based metric:");
|
||||
const heightMetric = await client.metrics.prices.price.usd.by.height.last(3);
|
||||
const heightMetric = await client.metrics.prices.spot.usd.by.height.last(3);
|
||||
console.log(
|
||||
` Total: ${heightMetric.total}, Start: ${heightMetric.start}, End: ${heightMetric.end}`,
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const DEFAULT_SEPARATORS = "_- ,:";
|
||||
const DEFAULT_TRIGRAM_BUDGET = 6;
|
||||
const DEFAULT_LIMIT = 100;
|
||||
const DEFAULT_MIN_SCORE = 2;
|
||||
|
||||
/**
|
||||
* Configuration for QuickMatch.
|
||||
@@ -15,6 +16,9 @@ export class QuickMatchConfig {
|
||||
/** @type {number} Number of trigram lookups for fuzzy matching (0-20) */
|
||||
trigramBudget = DEFAULT_TRIGRAM_BUDGET;
|
||||
|
||||
/** @type {number} Minimum trigram score required for fuzzy matches */
|
||||
minScore = DEFAULT_MIN_SCORE;
|
||||
|
||||
/**
|
||||
* Set maximum number of results.
|
||||
* @param {number} n
|
||||
@@ -42,6 +46,16 @@ export class QuickMatchConfig {
|
||||
this.separators = s;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set minimum trigram score for fuzzy matches.
|
||||
* Higher values require more trigram overlap, reducing noise.
|
||||
* @param {number} n - Minimum score (default: 2, min: 1)
|
||||
*/
|
||||
withMinScore(n) {
|
||||
this.minScore = Math.max(1, n);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,7 +193,7 @@ export class QuickMatch {
|
||||
minItemLength,
|
||||
});
|
||||
|
||||
const minScoreToInclude = Math.max(1, Math.ceil(hitCount / 2));
|
||||
const minScoreToInclude = Math.max(config.minScore, Math.ceil(hitCount / 2));
|
||||
|
||||
return this.rankedResults(scores, minScoreToInclude, limit);
|
||||
}
|
||||
|
||||
@@ -264,10 +264,8 @@ class AddressTxidsParam(TypedDict):
|
||||
"""
|
||||
Attributes:
|
||||
after_txid: Txid to paginate from (return transactions before this one)
|
||||
limit: Maximum number of results to return. Defaults to 25 if not specified.
|
||||
"""
|
||||
after_txid: Union[Txid, None]
|
||||
limit: int
|
||||
|
||||
class AddressValidation(TypedDict):
|
||||
"""
|
||||
@@ -767,10 +765,16 @@ class PaginatedMetrics(TypedDict):
|
||||
Attributes:
|
||||
current_page: Current page number (0-indexed)
|
||||
max_page: Maximum valid page index (0-indexed)
|
||||
metrics: List of metric names (max 1000 per page)
|
||||
total_count: Total number of metrics
|
||||
per_page: Results per page
|
||||
has_more: Whether more pages are available after the current one
|
||||
metrics: List of metric names
|
||||
"""
|
||||
current_page: int
|
||||
max_page: int
|
||||
total_count: int
|
||||
per_page: int
|
||||
has_more: bool
|
||||
metrics: List[str]
|
||||
|
||||
class Pagination(TypedDict):
|
||||
@@ -779,8 +783,10 @@ class Pagination(TypedDict):
|
||||
|
||||
Attributes:
|
||||
page: Pagination index
|
||||
per_page: Results per page (default: 1000, max: 1000)
|
||||
"""
|
||||
page: Optional[int]
|
||||
per_page: Optional[int]
|
||||
|
||||
class PoolBlockCounts(TypedDict):
|
||||
"""
|
||||
@@ -4481,7 +4487,7 @@ class MetricsTree_Market_Dca:
|
||||
def __init__(self, client: BrkClientBase, base_path: str = ''):
|
||||
self.sats_per_day: MetricPattern18[Sats] = MetricPattern18(client, 'dca_sats_per_day')
|
||||
self.period: MetricsTree_Market_Dca_Period = MetricsTree_Market_Dca_Period(client)
|
||||
self.class: MetricsTree_Market_Dca_Class = MetricsTree_Market_Dca_Class(client)
|
||||
self.class_: MetricsTree_Market_Dca_Class = MetricsTree_Market_Dca_Class(client)
|
||||
|
||||
class MetricsTree_Market_Technical_Rsi_1w:
|
||||
"""Metrics tree node."""
|
||||
@@ -5303,7 +5309,7 @@ class MetricsTree_Distribution_Cohorts_Utxo:
|
||||
self.under_age: MetricsTree_Distribution_Cohorts_Utxo_UnderAge = MetricsTree_Distribution_Cohorts_Utxo_UnderAge(client)
|
||||
self.over_age: MetricsTree_Distribution_Cohorts_Utxo_OverAge = MetricsTree_Distribution_Cohorts_Utxo_OverAge(client)
|
||||
self.epoch: MetricsTree_Distribution_Cohorts_Utxo_Epoch = MetricsTree_Distribution_Cohorts_Utxo_Epoch(client)
|
||||
self.class: MetricsTree_Distribution_Cohorts_Utxo_Class = MetricsTree_Distribution_Cohorts_Utxo_Class(client)
|
||||
self.class_: MetricsTree_Distribution_Cohorts_Utxo_Class = MetricsTree_Distribution_Cohorts_Utxo_Class(client)
|
||||
self.over_amount: MetricsTree_Distribution_Cohorts_Utxo_OverAmount = MetricsTree_Distribution_Cohorts_Utxo_OverAmount(client)
|
||||
self.amount_range: MetricsTree_Distribution_Cohorts_Utxo_AmountRange = MetricsTree_Distribution_Cohorts_Utxo_AmountRange(client)
|
||||
self.under_amount: MetricsTree_Distribution_Cohorts_Utxo_UnderAmount = MetricsTree_Distribution_Cohorts_Utxo_UnderAmount(client)
|
||||
@@ -6392,32 +6398,30 @@ class BrkClient(BrkClientBase):
|
||||
Endpoint: `GET /api/address/{address}`"""
|
||||
return self.get_json(f'/api/address/{address}')
|
||||
|
||||
def get_address_txs(self, address: Address, after_txid: Optional[str] = None, limit: Optional[float] = None) -> List[Txid]:
|
||||
"""Address transaction IDs.
|
||||
def get_address_txs(self, address: Address, after_txid: Optional[Txid] = None) -> List[Transaction]:
|
||||
"""Address transactions.
|
||||
|
||||
Get transaction IDs for an address, newest first. Use after_txid for pagination.
|
||||
Get transaction history for an address, sorted with newest first. Returns up to 50 mempool transactions plus the first 25 confirmed transactions. Use ?after_txid=<txid> for pagination.
|
||||
|
||||
*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions)*
|
||||
|
||||
Endpoint: `GET /api/address/{address}/txs`"""
|
||||
params = []
|
||||
if after_txid is not None: params.append(f'after_txid={after_txid}')
|
||||
if limit is not None: params.append(f'limit={limit}')
|
||||
query = '&'.join(params)
|
||||
path = f'/api/address/{address}/txs{"?" + query if query else ""}'
|
||||
return self.get_json(path)
|
||||
|
||||
def get_address_confirmed_txs(self, address: Address, after_txid: Optional[str] = None, limit: Optional[float] = None) -> List[Txid]:
|
||||
def get_address_confirmed_txs(self, address: Address, after_txid: Optional[Txid] = None) -> List[Transaction]:
|
||||
"""Address confirmed transactions.
|
||||
|
||||
Get confirmed transaction IDs for an address, 25 per page. Use ?after_txid=<txid> for pagination.
|
||||
Get confirmed transactions for an address, 25 per page. Use ?after_txid=<txid> for pagination.
|
||||
|
||||
*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-transactions-chain)*
|
||||
|
||||
Endpoint: `GET /api/address/{address}/txs/chain`"""
|
||||
params = []
|
||||
if after_txid is not None: params.append(f'after_txid={after_txid}')
|
||||
if limit is not None: params.append(f'limit={limit}')
|
||||
query = '&'.join(params)
|
||||
path = f'/api/address/{address}/txs/chain{"?" + query if query else ""}'
|
||||
return self.get_json(path)
|
||||
@@ -6568,7 +6572,7 @@ class BrkClient(BrkClientBase):
|
||||
Endpoint: `GET /api/metric/{metric}`"""
|
||||
return self.get_json(f'/api/metric/{metric}')
|
||||
|
||||
def get_metric(self, metric: Metric, index: Index, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[str] = None, format: Optional[Format] = None) -> Union[AnyMetricData, str]:
|
||||
def get_metric(self, metric: Metric, index: Index, start: Optional[RangeIndex] = None, end: Optional[RangeIndex] = None, limit: Optional[Limit] = None, format: Optional[Format] = None) -> Union[AnyMetricData, str]:
|
||||
"""Get metric data.
|
||||
|
||||
Fetch data for a specific metric at the given index. Use query parameters to filter by date range and format (json/csv).
|
||||
@@ -6585,7 +6589,7 @@ class BrkClient(BrkClientBase):
|
||||
return self.get_text(path)
|
||||
return self.get_json(path)
|
||||
|
||||
def get_metric_data(self, metric: Metric, index: Index, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[str] = None, format: Optional[Format] = None) -> Union[List[bool], str]:
|
||||
def get_metric_data(self, metric: Metric, index: Index, start: Optional[RangeIndex] = None, end: Optional[RangeIndex] = None, limit: Optional[Limit] = None, format: Optional[Format] = None) -> Union[List[bool], str]:
|
||||
"""Get raw metric data.
|
||||
|
||||
Returns just the data array without the MetricData wrapper. Supports the same range and format parameters as the standard endpoint.
|
||||
@@ -6634,7 +6638,7 @@ class BrkClient(BrkClientBase):
|
||||
Endpoint: `GET /api/metrics`"""
|
||||
return self.get_json('/api/metrics')
|
||||
|
||||
def get_metrics(self, metrics: Metrics, index: Index, start: Optional[str] = None, end: Optional[str] = None, limit: Optional[str] = None, format: Optional[Format] = None) -> Union[List[AnyMetricData], str]:
|
||||
def get_metrics(self, metrics: Metrics, index: Index, start: Optional[RangeIndex] = None, end: Optional[RangeIndex] = None, limit: Optional[Limit] = None, format: Optional[Format] = None) -> Union[List[AnyMetricData], str]:
|
||||
"""Bulk metric data.
|
||||
|
||||
Fetch multiple metrics in a single request. Supports filtering by index and date range. Returns an array of MetricData objects. For a single metric, use `get_metric` instead.
|
||||
@@ -6702,7 +6706,7 @@ class BrkClient(BrkClientBase):
|
||||
Endpoint: `GET /api/metrics/indexes`"""
|
||||
return self.get_json('/api/metrics/indexes')
|
||||
|
||||
def list_metrics(self, page: Optional[float] = None) -> PaginatedMetrics:
|
||||
def list_metrics(self, page: Optional[float] = None, per_page: Optional[float] = None) -> PaginatedMetrics:
|
||||
"""Metrics list.
|
||||
|
||||
Paginated flat list of all available metric names. Use `page` query param for pagination.
|
||||
@@ -6710,6 +6714,7 @@ class BrkClient(BrkClientBase):
|
||||
Endpoint: `GET /api/metrics/list`"""
|
||||
params = []
|
||||
if page is not None: params.append(f'page={page}')
|
||||
if per_page is not None: params.append(f'per_page={per_page}')
|
||||
query = '&'.join(params)
|
||||
path = f'/api/metrics/list{"?" + query if query else ""}'
|
||||
return self.get_json(path)
|
||||
|
||||
Reference in New Issue
Block a user