Skip to main content

satd_events_client/
lib.rs

1//! Async Rust client SDK for the satd Streaming Consumption API
2//! (`satd.events.v1`).
3//!
4//! It wraps the generated tonic client with a typed event model, auth metadata
5//! injection, and cursor capture, so a consumer writes against [`StreamClient`]
6//! instead of hand-rolling channels, metadata, and protobuf unwrapping.
7//!
8//! ```no_run
9//! use satd_events_client::{StreamClient, SubscribeOptions, Categories, Event};
10//!
11//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
12//! let mut client = StreamClient::builder("http://127.0.0.1:50051")
13//!     // Loopback, so the token never leaves the host. Off-host, use
14//!     // `.bearer_token(..)` with an `https://` endpoint — it refuses to send
15//!     // a credential over an unencrypted connection.
16//!     .insecure_bearer_token("…")
17//!     .keepalive_default()
18//!     .connect()
19//!     .await?;
20//!
21//! let mut events = client.subscribe(SubscribeOptions {
22//!     categories: Categories::MEMPOOL | Categories::CHAIN,
23//!     ..Default::default()
24//! }).await?;
25//!
26//! while let Some(event) = events.message().await? {
27//!     match event {
28//!         Event::BlockConnected { height, .. } => println!("block {height}"),
29//!         Event::MempoolEnter { fee, vsize, .. } => println!("tx {fee}/{vsize}"),
30//!         Event::Lagged { resume_cursor, .. } => {
31//!             // reconnect with `resume_cursor` to recover the gap
32//!             let _ = resume_cursor;
33//!         }
34//!         _ => {}
35//!     }
36//! }
37//! # Ok(())
38//! # }
39//! ```
40//!
41//! ## Scope (this revision)
42//!
43//! - [`StreamClient::subscribe`] — the full firehose with `from_cursor` replay
44//!   and cursor capture.
45//! - [`StreamClient::watch`] — opens the bidirectional stream and returns a
46//!   [`WatchHandle`] with typed helpers for every watch kind: `add_scripts`
47//!   (with per-script `min_value` floors), `add_outpoints`, `add_tx_lifecycle` /
48//!   `add_depth_alarms`, `add_descriptor`, and `add_script_prefixes` (the
49//!   privacy-preserving prefix watch), plus `set_categories` / `set_cursor` and
50//!   the matching `remove_*`. [`send_control`](WatchHandle::send_control) remains
51//!   for raw access.
52//!
53//! - [`StreamClient::resilient_subscribe`] — the firehose wrapped in a
54//!   [`ResilientSubscription`] that reconnects with backoff, persists and
55//!   replays the durable cursor (via a [`CursorStore`]), recovers from `Lagged`
56//!   per a [`LagPolicy`], and surfaces replay-truncation gaps.
57//! - [`StreamClient::resilient_watch`] — the bidirectional `Watch` stream
58//!   wrapped in a [`ResilientWatch`] that mirrors the watch-set and
59//!   re-registers it on reconnect (watch-sets are per-connection), then
60//!   re-anchors off the deterministic [`Event::CursorAccepted`] /
61//!   [`Event::CursorRejected`] results, retrying transient rejects in place and
62//!   surfacing the rest for a resnapshot. Integrators with a durable watch-set
63//!   source-of-truth can install a
64//!   [`watch_set_loader`](ResilientWatchConfig::watch_set_loader) to rebuild the
65//!   canonical set from that truth on every (re)connect.
66//! - [`PrefixWatcher`] (default-on `bitcoin` feature) — the privacy-preserving
67//!   prefix-watch local re-filter: decodes a [`PrefixMatch`]'s `raw_tx` and
68//!   recomputes `sha256(scriptPubKey)` to keep only true matches.
69//!
70//! ## TLS / mTLS (default-on `tls` feature)
71//!
72//! Builder methods encrypt the gRPC transport so a bearer token (and the event
73//! stream) never travels in cleartext:
74//!
75//! - [`tls`](StreamClientBuilder::tls) — TLS with the bundled Mozilla roots
76//!   (public-CA servers).
77//! - [`tls_ca_pem`](StreamClientBuilder::tls_ca_pem) — pin a private / self-signed
78//!   CA, the usual case for a satd node serving its own certificate.
79//! - [`tls_client_identity`](StreamClientBuilder::tls_client_identity) — present a
80//!   client certificate for mutual TLS (`-eventsgrpcmtls`).
81//! - [`tls_domain`](StreamClientBuilder::tls_domain) — override the verified
82//!   certificate name (SNI) when connecting by IP or through a proxy.
83//!
84//! ```no_run
85//! # #[cfg(feature = "tls")]
86//! # use satd_events_client::StreamClient;
87//! # #[cfg(feature = "tls")]
88//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
89//! let ca = std::fs::read("node-ca.pem")?;
90//! let client = StreamClient::builder("https://node.example:50051")
91//!     .tls_ca_pem(ca)
92//!     .bearer_token("…")
93//!     .connect()
94//!     .await?;
95//! # let _ = client;
96//! # Ok(())
97//! # }
98//! ```
99//!
100//! TLS uses the `ring` rustls provider. Opt out with `default-features = false`
101//! for a plaintext-only build — but note that such a build cannot encrypt
102//! *anything*: tonic only honours the `https://` scheme when it too is compiled
103//! with TLS, so without this feature an `https://` endpoint connects in
104//! cleartext. [`bearer_token`](StreamClientBuilder::bearer_token) therefore
105//! refuses every endpoint in a build without `tls`. The raw wire types are
106//! re-exported under [`proto`] for low-level use.
107//!
108//! ## Stability & versioning
109//!
110//! The SDK tracks the **additive `satd.events.v1` wire schema**, not the node's
111//! release cadence: new optional fields and event / watch kinds are added
112//! without breaking existing consumers, and the crate follows [semver]
113//! independently of the satd node version — a node and SDK do **not** need
114//! matching versions. The generated wire types are re-exported under [`proto`]
115//! so you can pin to the schema directly when a typed helper does not yet cover
116//! your case. Minimum supported Rust version (**MSRV**) is **1.93**; an MSRV
117//! bump is treated as a minor-version change.
118//!
119//! See the [wire/streaming spec][spec] for the underlying gRPC contract and the
120//! [`satd-events-proto`][proto-crate] crate for the generated types.
121//!
122//! [semver]: https://semver.org/
123//! [spec]: https://github.com/epochbtc/satd/blob/master/docs/api/streaming.md
124//! [proto-crate]: https://docs.rs/satd-events-proto
125
126#![cfg_attr(docsrs, feature(doc_cfg))]
127#![forbid(unsafe_code)]
128#![warn(missing_docs)]
129
130mod client;
131mod error;
132mod event;
133#[cfg(feature = "bitcoin")]
134mod prefix;
135mod resilience;
136mod resilient_watch;
137
138pub use client::{
139    AutoClose, Categories, EventStream, MAX_SP_LABELS_PER_TARGET, MAX_SP_TARGETS_PER_CONNECTION,
140    SilentPaymentTarget, StreamClient, StreamClientBuilder, SubscribeOptions, WatchHandle,
141};
142pub use error::StreamError;
143pub use event::{
144    display_hex, Cursor, CursorRejectReason, DescriptorMatch, Event, EvictReason, Outpoint,
145    PrefixMatch, RescanRejectReason, ScriptPrefix, SpentPrevout, StatusKind, StatusSeverity,
146    StatusState, TaprootOutput, TweakEntry, WatchSetRejectReason,
147};
148/// Parse a raw wire txid into a typed [`bitcoin::Txid`]. Requires the `bitcoin`
149/// feature.
150#[cfg(feature = "bitcoin")]
151pub use event::parse_txid;
152pub use resilience::{
153    Backoff, CursorStore, FileCursorStore, LagPolicy, NoopCursorStore, ResilientConfig,
154    ResilientSubscription,
155};
156pub use resilient_watch::{
157    ReloadError, ReloadSummary, ResilientWatch, ResilientWatchConfig, WatchSetBuilder,
158};
159
160#[cfg(feature = "bitcoin")]
161#[cfg_attr(docsrs, doc(cfg(feature = "bitcoin")))]
162pub use prefix::{
163    prefix_of, scripthash_of, FundingHit, PrefixHits, PrefixWatcher, SpendingHit,
164};
165
166/// The generated `satd.events.v1` wire types, for low-level control-message
167/// construction until typed helpers cover every case.
168pub use satd_events_proto::v1 as proto;