use satd_events_proto::v1 as pb;
pub use satd_events_proto::v1::Cursor;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Outpoint {
pub txid: Vec<u8>,
pub vout: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DescriptorMatch {
pub descriptor: String,
pub branch: u32,
pub derivation_index: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EvictReason {
Unspecified,
FullPool,
Expiry,
BlockConflict,
Policy,
Unknown(i32),
}
impl From<pb::EvictReason> for EvictReason {
fn from(r: pb::EvictReason) -> Self {
match r {
pb::EvictReason::Unspecified => EvictReason::Unspecified,
pb::EvictReason::FullPool => EvictReason::FullPool,
pb::EvictReason::Expiry => EvictReason::Expiry,
pb::EvictReason::BlockConflict => EvictReason::BlockConflict,
pb::EvictReason::Policy => EvictReason::Policy,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScriptPrefix {
pub prefix: Vec<u8>,
pub bits: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpentPrevout {
pub outpoint: Outpoint,
pub script_pubkey: Vec<u8>,
pub amount: Option<u64>,
}
impl From<pb::SpentPrevout> for SpentPrevout {
fn from(s: pb::SpentPrevout) -> Self {
SpentPrevout {
outpoint: Outpoint { txid: s.outpoint_txid, vout: s.outpoint_vout },
script_pubkey: s.script_pubkey,
amount: s.has_amount.then_some(s.amount),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrefixMatch {
pub prefix: ScriptPrefix,
pub raw_tx: Vec<u8>,
pub confirmed: bool,
pub height: u32,
pub matched_prevouts: Vec<SpentPrevout>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Event {
MempoolEnter {
txid: Vec<u8>,
fee: u64,
vsize: u64,
fee_rate_sat_per_kvb: u64,
time: u64,
},
MempoolLeaveConfirmed {
txid: Vec<u8>,
block_hash: Vec<u8>,
height: u32,
},
MempoolLeaveEvicted {
txid: Vec<u8>,
reason: EvictReason,
},
MempoolLeaveReplaced {
txid: Vec<u8>,
replacing_txid: Vec<u8>,
},
BlockConnected {
hash: Vec<u8>,
height: u32,
},
BlockDisconnected {
hash: Vec<u8>,
height: u32,
},
Reorg {
from_height: u32,
old_tip: Vec<u8>,
to_height: u32,
new_tip: Vec<u8>,
},
Heartbeat {
uptime_ns: u64,
},
OutpointSpent {
outpoint: Outpoint,
spending_txid: Vec<u8>,
spending_vin: u32,
confirmed: bool,
},
ScriptMatched {
scripthash: Vec<u8>,
txid: Vec<u8>,
is_output: bool,
index: u32,
confirmed: bool,
amount: Option<u64>,
raw_tx: Option<Vec<u8>>,
descriptors: Vec<DescriptorMatch>,
},
TxidMatched {
txid: Vec<u8>,
confirmed: bool,
height: u32,
},
TxidReplaced {
txid: Vec<u8>,
replacing_txid: Vec<u8>,
},
TxidEvicted {
txid: Vec<u8>,
reason: String,
},
TxidUnconfirmed {
txid: Vec<u8>,
prev_height: u32,
},
TxidDepthReached {
txid: Vec<u8>,
depth: u32,
height: u32,
},
TxidFinalized {
txid: Vec<u8>,
depth: u32,
height: u32,
},
PrefixMatched(PrefixMatch),
Lagged {
dropped_count: u64,
resume_cursor: Option<Cursor>,
},
ReplayGap {
resume_height: u32,
first_height: u32,
},
CursorAccepted {
from: Option<Cursor>,
clamped: bool,
earliest_replayed: u32,
},
CursorRejected {
reason: CursorRejectReason,
current_head: Option<Cursor>,
},
WatchSetReplaced {
added: u32,
removed: u32,
unchanged: u32,
},
WatchSetRejected {
reason: WatchSetRejectReason,
required: u64,
quota: u64,
},
RescanAccepted {
from_height: u32,
to_height: u32,
clamped: bool,
},
RescanRejected {
reason: RescanRejectReason,
tip_height: u32,
},
RescanComplete {
from_height: u32,
to_height: u32,
matches: u64,
},
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CursorRejectReason {
RateLimited,
ConcurrentReanchor,
EmptyCursor,
NoSource,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum WatchSetRejectReason {
QuotaExceeded,
CapExceeded,
Malformed,
Unknown,
}
fn watch_set_reject_reason(reason: i32) -> WatchSetRejectReason {
match pb::watch_set_rejected::Reason::try_from(reason) {
Ok(pb::watch_set_rejected::Reason::QuotaExceeded) => WatchSetRejectReason::QuotaExceeded,
Ok(pb::watch_set_rejected::Reason::CapExceeded) => WatchSetRejectReason::CapExceeded,
Ok(pb::watch_set_rejected::Reason::Malformed) => WatchSetRejectReason::Malformed,
Ok(pb::watch_set_rejected::Reason::Unspecified) | Err(_) => WatchSetRejectReason::Unknown,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RescanRejectReason {
RateLimited,
ConcurrentRescan,
InvalidRange,
RangeTooLarge,
NoSource,
EmptyWatchSet,
Unknown,
}
fn rescan_reject_reason(reason: i32) -> RescanRejectReason {
match pb::rescan_rejected::Reason::try_from(reason) {
Ok(pb::rescan_rejected::Reason::RateLimited) => RescanRejectReason::RateLimited,
Ok(pb::rescan_rejected::Reason::ConcurrentRescan) => RescanRejectReason::ConcurrentRescan,
Ok(pb::rescan_rejected::Reason::InvalidRange) => RescanRejectReason::InvalidRange,
Ok(pb::rescan_rejected::Reason::RangeTooLarge) => RescanRejectReason::RangeTooLarge,
Ok(pb::rescan_rejected::Reason::NoSource) => RescanRejectReason::NoSource,
Ok(pb::rescan_rejected::Reason::EmptyWatchSet) => RescanRejectReason::EmptyWatchSet,
Ok(pb::rescan_rejected::Reason::Unspecified) | Err(_) => RescanRejectReason::Unknown,
}
}
impl From<pb::NodeEvent> for Event {
fn from(ev: pb::NodeEvent) -> Self {
use pb::node_event::Body;
let Some(body) = ev.body else { return Event::Unknown };
match body {
Body::Mempool(m) => mempool_event(m),
Body::Chain(c) => chain_event(c),
Body::Heartbeat(h) => Event::Heartbeat { uptime_ns: h.uptime_ns },
Body::OutpointSpent(o) => Event::OutpointSpent {
outpoint: Outpoint { txid: o.outpoint_txid, vout: o.outpoint_vout },
spending_txid: o.spending_txid,
spending_vin: o.spending_vin,
confirmed: o.confirmed,
},
Body::ScriptMatched(s) => Event::ScriptMatched {
scripthash: s.scripthash,
txid: s.txid,
is_output: s.is_output,
index: s.index,
confirmed: s.confirmed,
amount: s.has_amount.then_some(s.amount),
raw_tx: (!s.raw_tx.is_empty()).then_some(s.raw_tx),
descriptors: s
.descriptor_matches
.into_iter()
.map(|d| DescriptorMatch {
descriptor: d.descriptor,
branch: d.branch,
derivation_index: d.derivation_index,
})
.collect(),
},
Body::TxidMatched(t) => Event::TxidMatched {
txid: t.txid,
confirmed: t.confirmed,
height: t.height,
},
Body::TxidReplaced(t) => Event::TxidReplaced {
txid: t.txid,
replacing_txid: t.replacing_txid,
},
Body::TxidEvicted(t) => Event::TxidEvicted { txid: t.txid, reason: t.reason },
Body::TxidUnconfirmed(t) => Event::TxidUnconfirmed {
txid: t.txid,
prev_height: t.prev_height,
},
Body::TxidDepthReached(t) => Event::TxidDepthReached {
txid: t.txid,
depth: t.depth,
height: t.height,
},
Body::TxidFinalized(t) => Event::TxidFinalized {
txid: t.txid,
depth: t.depth,
height: t.height,
},
Body::PrefixMatched(p) => match p.prefix {
Some(sp) => Event::PrefixMatched(PrefixMatch {
prefix: ScriptPrefix { prefix: sp.prefix, bits: sp.bits },
raw_tx: p.raw_tx,
confirmed: p.confirmed,
height: p.height,
matched_prevouts: p.matched_prevouts.into_iter().map(Into::into).collect(),
}),
None => Event::Unknown,
},
Body::Lagged(l) => Event::Lagged {
dropped_count: l.dropped_count,
resume_cursor: l.resume_cursor,
},
Body::SetCursorResult(r) => match r.outcome {
Some(pb::set_cursor_result::Outcome::Accepted(a)) => Event::CursorAccepted {
from: a.from,
clamped: a.clamped,
earliest_replayed: a.earliest_replayed,
},
Some(pb::set_cursor_result::Outcome::Rejected(rj)) => Event::CursorRejected {
reason: cursor_reject_reason(rj.reason),
current_head: rj.current_head,
},
None => Event::Unknown,
},
Body::SetWatchSetResult(r) => match r.outcome {
Some(pb::watch_set_result::Outcome::Accepted(a)) => Event::WatchSetReplaced {
added: a.added,
removed: a.removed,
unchanged: a.unchanged,
},
Some(pb::watch_set_result::Outcome::Rejected(rj)) => Event::WatchSetRejected {
reason: watch_set_reject_reason(rj.reason),
required: rj.required,
quota: rj.quota,
},
None => Event::Unknown,
},
Body::RescanResult(r) => match r.outcome {
Some(pb::rescan_result::Outcome::Accepted(a)) => Event::RescanAccepted {
from_height: a.from_height,
to_height: a.to_height,
clamped: a.clamped,
},
Some(pb::rescan_result::Outcome::Rejected(rj)) => Event::RescanRejected {
reason: rescan_reject_reason(rj.reason),
tip_height: rj.tip_height,
},
None => Event::Unknown,
},
Body::RescanComplete(c) => Event::RescanComplete {
from_height: c.from_height,
to_height: c.to_height,
matches: c.matches,
},
}
}
}
fn cursor_reject_reason(reason: i32) -> CursorRejectReason {
match pb::cursor_rejected::Reason::try_from(reason) {
Ok(pb::cursor_rejected::Reason::RateLimited) => CursorRejectReason::RateLimited,
Ok(pb::cursor_rejected::Reason::ConcurrentReanchor) => {
CursorRejectReason::ConcurrentReanchor
}
Ok(pb::cursor_rejected::Reason::EmptyCursor) => CursorRejectReason::EmptyCursor,
Ok(pb::cursor_rejected::Reason::NoSource) => CursorRejectReason::NoSource,
Ok(pb::cursor_rejected::Reason::Unspecified) | Err(_) => CursorRejectReason::Unknown,
}
}
fn mempool_event(m: pb::MempoolEvent) -> Event {
use pb::mempool_event::Body;
match m.body {
Some(Body::Enter(e)) => Event::MempoolEnter {
txid: e.txid,
fee: e.fee,
vsize: e.vsize,
fee_rate_sat_per_kvb: e.fee_rate_sat_per_kvb,
time: e.time,
},
Some(Body::LeaveConfirmed(e)) => Event::MempoolLeaveConfirmed {
txid: e.txid,
block_hash: e.block_hash,
height: e.height,
},
Some(Body::LeaveEvicted(e)) => {
let reason = e.reason().into();
Event::MempoolLeaveEvicted { txid: e.txid, reason }
}
Some(Body::LeaveReplaced(e)) => Event::MempoolLeaveReplaced {
txid: e.txid,
replacing_txid: e.replacing_txid,
},
None => Event::Unknown,
}
}
fn chain_event(c: pb::ChainEvent) -> Event {
use pb::chain_event::Body;
match c.body {
Some(Body::BlockConnected(b)) => Event::BlockConnected { hash: b.hash, height: b.height },
Some(Body::BlockDisconnected(b)) => {
Event::BlockDisconnected { hash: b.hash, height: b.height }
}
Some(Body::Reorg(r)) => Event::Reorg {
from_height: r.from_height,
old_tip: r.old_tip,
to_height: r.to_height,
new_tip: r.new_tip,
},
None => Event::Unknown,
}
}