Skip to content

Commit

Permalink
Merge pull request #80 from mempool/mononaut/address-summary
Browse files Browse the repository at this point in the history
Add /address/:addr/txs/summary endpoint
  • Loading branch information
softsimon authored Mar 20, 2024
2 parents f85bf8a + 9555742 commit d4f788f
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 3 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down
110 changes: 110 additions & 0 deletions src/new_index/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,108 @@ impl ChainQuery {
)
}

pub fn summary(
&self,
scripthash: &[u8],
last_seen_txid: Option<&Txid>,
limit: usize,
) -> Vec<TxHistorySummary> {
// 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<TxHistorySummary> {
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<Txid, TxHistorySummary> = 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::<Vec<TxHistorySummary>>();
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],
Expand Down Expand Up @@ -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,
Expand Down
34 changes: 33 additions & 1 deletion src/rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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::<usize>().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"),
Expand Down

0 comments on commit d4f788f

Please sign in to comment.