diff --git a/Cargo.toml b/Cargo.toml index 99b963ef..58c5b579 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,8 +21,8 @@ name = "electrs" [features] default = [] -liquid = [ "elements" ] -electrum-discovery = [ "electrum-client"] +liquid = ["elements"] +electrum-discovery = ["electrum-client"] [dependencies] arrayref = "0.3.6" diff --git a/src/config.rs b/src/config.rs index 2c428048..8278d985 100644 --- a/src/config.rs +++ b/src/config.rs @@ -59,6 +59,7 @@ pub struct Config { pub rest_default_block_limit: usize, pub rest_default_chain_txs_per_page: usize, pub rest_default_max_mempool_txs: usize, + pub rest_default_max_address_summary_txs: usize, pub rest_max_mempool_page_size: usize, pub rest_max_mempool_txid_page_size: usize, @@ -240,6 +241,12 @@ impl Config { .help("The default number of mempool transactions returned by the txs endpoints.") .default_value("50") ) + .arg( + Arg::with_name("rest_default_max_address_summary_txs") + .long("rest-default-max-address-summary-txs") + .help("The default number of transactions returned by the address summary endpoints.") + .default_value("5000") + ) .arg( Arg::with_name("rest_max_mempool_page_size") .long("rest-max-mempool-page-size") @@ -505,6 +512,11 @@ impl Config { "rest_default_max_mempool_txs", usize ), + rest_default_max_address_summary_txs: value_t_or_exit!( + m, + "rest_default_max_address_summary_txs", + usize + ), rest_max_mempool_page_size: value_t_or_exit!(m, "rest_max_mempool_page_size", usize), rest_max_mempool_txid_page_size: value_t_or_exit!( m, diff --git a/src/new_index/schema.rs b/src/new_index/schema.rs index 1fbe4830..00ee3e89 100644 --- a/src/new_index/schema.rs +++ b/src/new_index/schema.rs @@ -512,6 +512,108 @@ impl ChainQuery { ) } + pub fn summary( + &self, + scripthash: &[u8], + last_seen_txid: Option<&Txid>, + limit: usize, + ) -> Vec { + // scripthash lookup + self._summary(b'H', scripthash, last_seen_txid, limit) + } + + fn _summary( + &self, + code: u8, + hash: &[u8], + last_seen_txid: Option<&Txid>, + limit: usize, + ) -> Vec { + let _timer_scan = self.start_timer("address_summary"); + let rows = self + .history_iter_scan_reverse(code, hash) + .map(TxHistoryRow::from_row) + .map(|row| (row.get_txid(), row.key.txinfo)) + .skip_while(|(txid, _)| { + // skip until we reach the last_seen_txid + last_seen_txid.map_or(false, |last_seen_txid| last_seen_txid != txid) + }) + .skip_while(|(txid, _)| { + // skip the last_seen_txid itself + last_seen_txid.map_or(false, |last_seen_txid| last_seen_txid == txid) + }) + .filter_map(|(txid, info)| { + self.tx_confirming_block(&txid) + .map(|b| (txid, info, b.height, b.time)) + }); + + // collate utxo funding/spending events by transaction + let mut map: HashMap = HashMap::new(); + for (txid, info, height, time) in rows { + if !map.contains_key(&txid) && map.len() == limit { + break; + } + match info { + #[cfg(not(feature = "liquid"))] + TxHistoryInfo::Funding(info) => { + map.entry(txid) + .and_modify(|tx| { + tx.value = tx.value.saturating_add(info.value.try_into().unwrap_or(0)) + }) + .or_insert(TxHistorySummary { + txid, + value: info.value.try_into().unwrap_or(0), + height, + time, + }); + } + #[cfg(not(feature = "liquid"))] + TxHistoryInfo::Spending(info) => { + map.entry(txid) + .and_modify(|tx| { + tx.value = tx.value.saturating_sub(info.value.try_into().unwrap_or(0)) + }) + .or_insert(TxHistorySummary { + txid, + value: 0_i64.saturating_sub(info.value.try_into().unwrap_or(0)), + height, + time, + }); + } + #[cfg(feature = "liquid")] + TxHistoryInfo::Funding(_info) => { + map.entry(txid).or_insert(TxHistorySummary { + txid, + value: 0, + height, + time, + }); + } + #[cfg(feature = "liquid")] + TxHistoryInfo::Spending(_info) => { + map.entry(txid).or_insert(TxHistorySummary { + txid, + value: 0, + height, + time, + }); + } + #[cfg(feature = "liquid")] + _ => {} + } + } + + let mut tx_summaries = map.into_values().collect::>(); + tx_summaries.sort_by(|a, b| { + if a.height == b.height { + a.value.cmp(&b.value) + } else { + b.height.cmp(&a.height) + } + }); + tx_summaries + } + pub fn history( &self, scripthash: &[u8], @@ -1573,6 +1675,14 @@ impl TxHistoryInfo { } } +#[derive(Serialize, Deserialize)] +pub struct TxHistorySummary { + txid: Txid, + height: usize, + value: i64, + time: u32, +} + #[derive(Serialize, Deserialize)] struct TxEdgeKey { code: u8, diff --git a/src/rest.rs b/src/rest.rs index fdddce6a..df085441 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -22,7 +22,7 @@ use prometheus::{HistogramOpts, HistogramVec}; use tokio::sync::oneshot; use hyperlocal::UnixServerExt; -use std::fs; +use std::{cmp, fs}; #[cfg(feature = "liquid")] use { crate::elements::{peg::PegoutValue, AssetSorting, IssuanceValue}, @@ -957,6 +957,38 @@ fn handle_request( json_response(prepare_txs(txs, query, config), TTL_SHORT) } + ( + &Method::GET, + Some(script_type @ &"address"), + Some(script_str), + Some(&"txs"), + Some(&"summary"), + last_seen_txid, + ) + | ( + &Method::GET, + Some(script_type @ &"scripthash"), + Some(script_str), + Some(&"txs"), + Some(&"summary"), + last_seen_txid, + ) => { + let script_hash = to_scripthash(script_type, script_str, config.network_type)?; + let last_seen_txid = last_seen_txid.and_then(|txid| Txid::from_hex(txid).ok()); + let max_txs = cmp::min( + config.rest_default_max_address_summary_txs, + query_params + .get("max_txs") + .and_then(|s| s.parse::().ok()) + .unwrap_or(config.rest_default_max_address_summary_txs), + ); + + let summary = query + .chain() + .summary(&script_hash[..], last_seen_txid.as_ref(), max_txs); + + json_response(summary, TTL_SHORT) + } ( &Method::GET, Some(script_type @ &"address"),