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 crate version is the satd version it shipped with, and follows
111//! [semver]. An SDK works with any node on the same event schema whose version is
112//! at or above its own; newer nodes only add fields and event kinds. When a
113//! stream opens, the SDK compares the node's advertised version with its own:
114//! one minor version behind logs a `tracing` warning, two or more (or a major
115//! version) behind is refused with [`StreamError::NodeTooOld`] unless the client
116//! was built with [`allow_old_node`](StreamClientBuilder::allow_old_node), and a
117//! different schema is always refused with [`StreamError::SchemaMismatch`]. A
118//! node older than 0.6.0 advertises nothing and counts as 0.5. See
119//! [`StreamClient::node_version`]. The generated wire types are re-exported under [`proto`]
120//! so you can pin to the schema directly when a typed helper does not yet cover
121//! your case. Minimum supported Rust version (**MSRV**) is **1.93**; an MSRV
122//! bump is treated as a minor-version change.
123//!
124//! See the [wire/streaming spec][spec] for the underlying gRPC contract and the
125//! [`satd-events-proto`][proto-crate] crate for the generated types.
126//!
127//! [semver]: https://semver.org/
128//! [spec]: https://github.com/epochbtc/satd/blob/master/docs/api/streaming.md
129//! [proto-crate]: https://docs.rs/satd-events-proto
130
131#![cfg_attr(docsrs, feature(doc_cfg))]
132#![forbid(unsafe_code)]
133#![warn(missing_docs)]
134
135mod client;
136mod compat;
137mod error;
138mod event;
139#[cfg(feature = "bitcoin")]
140mod prefix;
141mod resilience;
142mod resilient_watch;
143
144pub use client::{
145    AutoClose, Categories, EventStream, MAX_SP_LABELS_PER_TARGET, MAX_SP_TARGETS_PER_CONNECTION,
146    SilentPaymentTarget, StreamClient, StreamClientBuilder, SubscribeOptions, WatchHandle,
147};
148pub use error::StreamError;
149pub use event::{
150    display_hex, Cursor, CursorRejectReason, DescriptorMatch, Event, EvictReason, Outpoint,
151    PrefixMatch, RejectedDescriptor, RescanRejectReason, ScriptPrefix, SpentPrevout, StatusKind,
152    StatusSeverity, StatusState, TaprootOutput, TweakEntry, WatchAddKind, WatchAddRejectReason,
153    WatchAddRejected, WatchSetRejectReason,
154};
155/// Parse a raw wire txid into a typed [`bitcoin::Txid`]. Requires the `bitcoin`
156/// feature.
157#[cfg(feature = "bitcoin")]
158pub use event::parse_txid;
159pub use resilience::{
160    Backoff, CursorStore, FileCursorStore, LagPolicy, NoopCursorStore, ResilientConfig,
161    ResilientSubscription,
162};
163pub use resilient_watch::{
164    ReloadError, ReloadSummary, ResilientWatch, ResilientWatchConfig, WatchSetBuilder,
165};
166
167#[cfg(feature = "bitcoin")]
168#[cfg_attr(docsrs, doc(cfg(feature = "bitcoin")))]
169pub use prefix::{
170    prefix_of, scripthash_of, FundingHit, PrefixHits, PrefixWatcher, SpendingHit,
171};
172
173/// The generated `satd.events.v1` wire types, for low-level control-message
174/// construction until typed helpers cover every case.
175pub use satd_events_proto::v1 as proto;