Expand description
Async Rust client SDK for the satd Streaming Consumption API
(satd.events.v1).
It wraps the generated tonic client with a typed event model, auth metadata
injection, and cursor capture, so a consumer writes against StreamClient
instead of hand-rolling channels, metadata, and protobuf unwrapping.
use satd_events_client::{StreamClient, SubscribeOptions, Categories, Event};
let mut client = StreamClient::builder("http://127.0.0.1:50051")
// Loopback, so the token never leaves the host. Off-host, use
// `.bearer_token(..)` with an `https://` endpoint — it refuses to send
// a credential over an unencrypted connection.
.insecure_bearer_token("…")
.keepalive_default()
.connect()
.await?;
let mut events = client.subscribe(SubscribeOptions {
categories: Categories::MEMPOOL | Categories::CHAIN,
..Default::default()
}).await?;
while let Some(event) = events.message().await? {
match event {
Event::BlockConnected { height, .. } => println!("block {height}"),
Event::MempoolEnter { fee, vsize, .. } => println!("tx {fee}/{vsize}"),
Event::Lagged { resume_cursor, .. } => {
// reconnect with `resume_cursor` to recover the gap
let _ = resume_cursor;
}
_ => {}
}
}§Scope (this revision)
-
StreamClient::subscribe— the full firehose withfrom_cursorreplay and cursor capture. -
StreamClient::watch— opens the bidirectional stream and returns aWatchHandlewith typed helpers for every watch kind:add_scripts(with per-scriptmin_valuefloors),add_outpoints,add_tx_lifecycle/add_depth_alarms,add_descriptor, andadd_script_prefixes(the privacy-preserving prefix watch), plusset_categories/set_cursorand the matchingremove_*.send_controlremains for raw access. -
StreamClient::resilient_subscribe— the firehose wrapped in aResilientSubscriptionthat reconnects with backoff, persists and replays the durable cursor (via aCursorStore), recovers fromLaggedper aLagPolicy, and surfaces replay-truncation gaps. -
StreamClient::resilient_watch— the bidirectionalWatchstream wrapped in aResilientWatchthat mirrors the watch-set and re-registers it on reconnect (watch-sets are per-connection), then re-anchors off the deterministicEvent::CursorAccepted/Event::CursorRejectedresults, retrying transient rejects in place and surfacing the rest for a resnapshot. Integrators with a durable watch-set source-of-truth can install awatch_set_loaderto rebuild the canonical set from that truth on every (re)connect. -
PrefixWatcher(default-onbitcoinfeature) — the privacy-preserving prefix-watch local re-filter: decodes aPrefixMatch’sraw_txand recomputessha256(scriptPubKey)to keep only true matches.
§TLS / mTLS (default-on tls feature)
Builder methods encrypt the gRPC transport so a bearer token (and the event stream) never travels in cleartext:
tls— TLS with the bundled Mozilla roots (public-CA servers).tls_ca_pem— pin a private / self-signed CA, the usual case for a satd node serving its own certificate.tls_client_identity— present a client certificate for mutual TLS (-eventsgrpcmtls).tls_domain— override the verified certificate name (SNI) when connecting by IP or through a proxy.
let ca = std::fs::read("node-ca.pem")?;
let client = StreamClient::builder("https://node.example:50051")
.tls_ca_pem(ca)
.bearer_token("…")
.connect()
.await?;TLS uses the ring rustls provider. Opt out with default-features = false
for a plaintext-only build — but note that such a build cannot encrypt
anything: tonic only honours the https:// scheme when it too is compiled
with TLS, so without this feature an https:// endpoint connects in
cleartext. bearer_token therefore
refuses every endpoint in a build without tls. The raw wire types are
re-exported under proto for low-level use.
§Stability & versioning
The crate version is the satd version it shipped with, and follows
semver. An SDK works with any node on the same event schema whose version is
at or above its own; newer nodes only add fields and event kinds. When a
stream opens, the SDK compares the node’s advertised version with its own:
one minor version behind logs a tracing warning, two or more (or a major
version) behind is refused with StreamError::NodeTooOld unless the client
was built with allow_old_node, and a
different schema is always refused with StreamError::SchemaMismatch. A
node older than 0.6.0 advertises nothing and counts as 0.5. See
StreamClient::node_version. The generated wire types are re-exported under proto
so you can pin to the schema directly when a typed helper does not yet cover
your case. Minimum supported Rust version (MSRV) is 1.93; an MSRV
bump is treated as a minor-version change.
See the wire/streaming spec for the underlying gRPC contract and the
satd-events-proto crate for the generated types.
Modules§
- proto
- The generated
satd.events.v1wire types, for low-level control-message construction until typed helpers cover every case.
Structs§
- Backoff
- Exponential reconnect backoff. Delays grow
initial * multiplier^attempt, capped atmax. No jitter is applied (a single client reconnecting to a single node needs none); add it externally if fanning many clients at one server. - Categories
- Category bitfield helpers for
SubscribeOptions::categories. - Cursor
- Durable resume position, re-exported from the wire schema. Persist the
value returned by
EventStream::cursorand present it again asfrom_cursorto resume. Durable resume position. Confirmed-side cursors are(height, tx_index)— per-transaction, so a client can resume mid-block after a disconnect.mempool_seqis a best-effort high-water mark for the mempool side (advisory; resets on daemon restart). A client persists thecursorfrom the lastNodeEventit durably processed and presents it asfrom_cursoron reconnect. - Descriptor
Match - Descriptor attribution for a
ScriptMatched: which descriptor watch a matched scripthash belongs to, and the exact coordinate the server derived it at. - Event
Stream - A live stream of typed
Events, shared bySubscribeandWatch. - File
Cursor Store - A
CursorStorebacked by a single file, written atomically (temp file + rename) so a crash mid-write never leaves a torn cursor. The on-disk format is one line of four whitespace-separated integers —height tx_index mempool_seq instance_id— stable and trivially inspectable. A missing file loads asNone. - Funding
Hit bitcoin - An output (funding side) of a delivered tx that pays a watched script.
- Noop
Cursor Store - A
CursorStorethat persists nothing:loadis alwaysNone,saveis a no-op. The default — reconnects still resume from the in-memory last cursor, but a process restart starts forward-only. - Outpoint
- An outpoint (
txid:vout), raw bytes. - Prefix
Hits bitcoin - The result of re-filtering one
PrefixMatchagainst the watched set: the outputs and spent prevouts that are genuine matches, plus any spend-side prevouts the server did not retain and so could not be filtered locally. - Prefix
Match - A transaction that fell inside a watched script-prefix bucket. Carries the full serialized tx so the client filters the bucket against its real scripts locally (the privacy contract — the server only learns the bucket).
- Prefix
Watcher bitcoin - Holds the consumer’s real scriptPubKeys and re-filters coarse prefix-bucket deliveries down to true matches.
- Rejected
Descriptor - A descriptor named by a
WatchAddRejected. - Reload
Summary - The outcome of
ResilientWatch::reload. - Resilient
Config - Bundles the resilience knobs for
StreamClient::resilient_subscribe. - Resilient
Subscription - A firehose that reconnects, replays from a persisted cursor, and recovers
from lag on the consumer’s behalf. Construct it with
StreamClient::resilient_subscribeand drive it by callingnextin a loop. - Resilient
Watch - A
Watchstream that reconnects, re-registers its watch-set, and re-anchors off the deterministicEvent::CursorAccepted/Event::CursorRejectedresults on the caller’s behalf. - Resilient
Watch Config - Knobs for
StreamClient::resilient_watch. Reuses theBackoffandCursorStorefrom theResilientSubscriptionresilience layer. - Script
Prefix - A k-bit prefix of
sha256(scriptPubKey). - Silent
Payment Target - A BIP 352 scan-key watch target (Tier 2): a
(scan_secret, spend_pubkey)pair plus optional labels, registered viaWatchHandle::add_silent_payments. The node runs the ECDH match and pushesEvent::SilentPaymentMatched. - Spending
Hit bitcoin - A spent prevout (spending side) of a delivered tx that consumed a watched
script. Only produced for prevouts the server retained the script for;
unretained ones surface in
PrefixHits::unresolved. - Spent
Prevout - A spent prevout that matched a prefix bucket (the spend side of a
Event::PrefixMatched). - Stream
Client - An async client for the satd
satd.events.v1streaming API. - Stream
Client Builder - Builder for a
StreamClient. - Subscribe
Options - Options for a
StreamClient::subscribefirehose. - Taproot
Output - One of a transaction’s taproot outputs — a scan candidate. Carried in
TweakEntry::taproot_outputsso a client can confirm a derived output key against the actual on-chain output without fetching the transaction. - Tweak
Entry - One transaction’s public silent-payment tweak, carried in a
Event::BlockTweaksfor Tier 1 client-side scanning. - Watch
AddRejected - The payload of
Event::WatchAddRejected: which add was refused, why, and the items it named. Only the item field forkindis set. - Watch
Handle - A handle to send control messages on a bidirectional
Watchstream. - Watch
SetBuilder - The canonical watch-set declared by a
watch_set_loaderon every (re)connect. It is a thin recording facade over a fresh [WatchSetMirror]: eachadd_*/set_categoriescall records interest the way the matchingResilientWatchmethod would, so the populated set renders to exactly the same control messages a manual replay would. The wrapper adopts the builder’s set as the mirror once the loader returns, then re-registers it and re-anchors as usual.
Enums§
- Auto
Close - Auto-close policy for a transaction lifecycle watch: self-evict and emit
TxidFinalizedonce the tx is buried this many confirmations deep. - Cursor
Reject Reason - Why a mid-stream re-anchor was declined by the server (see
Event::CursorRejected). - Event
- A typed streaming event — the flat mirror of
NodeEvent.body. - Evict
Reason - Why a transaction left the mempool by policy.
- LagPolicy
- What the resilience loop does with a
Laggednotice. - Reload
Error - Why a
ResilientWatch::reloadcould not run. - Rescan
Reject Reason - Why a bounded historical rescan was declined by the server (see
Event::RescanRejected). - Status
Kind - Which node-health condition an
Event::Statusdescribes. - Status
Severity - How loud a health condition is.
- Status
State - Level-triggered lifecycle of a health condition.
- Stream
Error - An error from the streaming client.
- Watch
AddKind - Which kind of add a
WatchAddRejectedrefers to. - Watch
AddReject Reason - Why the server refused an incremental add (see
Event::WatchAddRejected). - Watch
SetReject Reason - Why an atomic watch-set replace was declined by the server (see
Event::WatchSetRejected).
Constants§
- MAX_
SP_ LABELS_ PER_ TARGET - Server-enforced cap on the number of distinct labels one scan-key target
may carry (mirrors the node’s
MAX_SP_LABELS_PER_TARGET). The server rejects an over-label target; enforcing the same bound client-side turns that into a deterministic error at the call site instead of a silent skip. - MAX_
SP_ TARGETS_ PER_ CONNECTION - Server-enforced cap on the number of scan-key targets per connection (mirrors
the node’s
MAX_SP_TARGETS_PER_CONNECTION). The server silently sheds an over-cap add; a statefulResilientWatchenforces this bound before recording/sending so it never mirrors and replays a target the server dropped.
Traits§
- Cursor
Store - Persists the durable resume
Cursoracross reconnects and process restarts. The resilience loop loads it on (re)connect and persists it commit-on-poll: a delivered event’s cursor is written only on the nextnextcall, i.e. once the caller has come back for the following event (an implicit ack). The store therefore never advances past an event the caller has not yet received, so a crash mid-processing replays that event on resume — at-least-once, not at-most-once. (A consumer that needs exactly-once still dedups on its own side keyed by the(height, hash)it processes.)
Functions§
- display_
hex - Render a raw wire hash/txid (32 bytes, internal / consensus byte order, as every hash and txid on this API is carried) as a lowercase hex string in the reversed display order used by block explorers and Bitcoin Core JSON-RPC.
- parse_
txid bitcoin - Parse a raw wire txid into a typed
bitcoin::Txid. Requires thebitcoinfeature. Parse a raw wire txid (32 bytes, internal/consensus byte order) into a typedbitcoin::Txid. The returned value’sDisplayis the usual explorer / JSON-RPC order, and it hands straight tobitcoin-ecosystem APIs (bitcoincore-rpc, comparison againstgetrawtransaction, …). - prefix_
of bitcoin - Derive the
(prefix, bits)registration tuple for a scriptPubKey: the topceil(bits/8)bytes of its scripthash.bitsis clamped to1..=32— the server buckets on the top 32 bits of the scripthash only, so a wider prefix is meaningless and would be silently dropped;WatchHandle::add_script_prefixesre-validates the same bound before the wire. Bits belowbitsare masked off. The wire carries whole bytes, so a 12-bit bucket ships 2 bytes while the server keys on the top 12 bits and masks the rest away (prefix_bucket_key,node/src/events/watch.rs). Sending those 4 extra bits unmasked hands the server 16× more of the scripthash than the declared bucket width — a free narrowing of the anonymity set the consumer asked for. Masking costs nothing: the bucket key is identical either way. - scripthash_
of bitcoin sha256(scriptPubKey)— the 32-byte scripthash the server keys watches on (the prefix bucket is its topceil(bits/8)bytes, big-endian).