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;